How to use React TransitionMotion willEnter() - javascript

Using React Motion's TransitionMotion, I want to animate 1 or more boxes in and out. When a box enters the view, it's width and height should go from 0 pixels to 200 pixels and it's opacity should go from 0 to 1. When the box leaves the view, the reverse should happen (width/height = 0, opacity = 0)
I have tried to solve this problem here http://codepen.io/danijel/pen/RaboxO but my code is unable to transition the box in correctly. The box's style jumps immediately to a width/height of 200 pixels instead of transitioning in.
What is wrong with the code?
let Motion = ReactMotion.Motion
let TransitionMotion = ReactMotion.TransitionMotion
let spring = ReactMotion.spring
let presets = ReactMotion.presets
const Demo = React.createClass({
getInitialState() {
return {
items: []
}
},
componentDidMount() {
let ctr = 0
setInterval(() => {
ctr++
console.log(ctr)
if (ctr % 2 == 0) {
this.setState({
items: [{key: 'b', width: 200, height: 200, opacity: 1}], // fade box in
});
} else {
this.setState({
items: [], // fade box out
});
}
}, 1000)
},
willLeave() {
// triggered when c's gone. Keeping c until its width/height reach 0.
return {width: spring(0), height: spring(0), opacity: spring(0)};
},
willEnter() {
return {width: 0, height: 0, opacity: 1};
},
render() {
return (
<TransitionMotion
willEnter={this.willEnter}
willLeave={this.willLeave}
defaultStyles={this.state.items.map(item => ({
key: item.key,
style: {
width: 0,
height: 0,
opacity: 0
},
}))}
styles={this.state.items.map(item => ({
key: item.key,
style: {
width: item.width,
height: item.height,
opacity: item.opacity
},
}))}
>
{interpolatedStyles =>
<div>
{interpolatedStyles.map(config => {
return <div key={config.key} style={{...config.style, backgroundColor: 'yellow'}}>
<div className="label">{config.style.width}</div>
</div>
})}
</div>
}
</TransitionMotion>
);
},
});
ReactDOM.render(<Demo />, document.getElementById('app'));

As per the documentation of styles under the TransitionMotion section (and I don't claim to have understood all of it entirely :)):
styles: ... an array of TransitionStyle ...
The key thing to note here is that there are 2 types of style objects that this library deals with (or at least this TransitionMotion part of it) and it calls them TransitionStyle and TransitionPlainStyle.
The previous values passed into styles attribute were of TransitionPlainStyle. Changing them to TransitionStyle magically starts animating the Enter sequence.
You can read more about 2 different types mentioned above over here.
styles={this.state.items.map(item => ({
key: item.key,
style: {
width: spring(item.width),
height: spring(item.height),
opacity: spring(item.opacity)
}
}))}
Forked codepen demo.
Again, I do not fully understand the inner workings of it just yet. I just know that your styles had to be changed in the above way to make it work.
I will be happy if someone can educate me on this as well.
Hope this helps.

Related

Framer Motion StaggerChildren Animation on Child Removal do not Trigger Animation

I have a list (ParentBox.tsx) that contains many items (Box.tsx). When clicking the Add button, the ParentBox has one additional unique Box. The animation works fine. However, there are two scenarios where it does not:
When I click on the Box, it removes the item from the list. Framer Motion removes the Box from the user interface without exit animation.
When clicking "Remove All", the whole list of items is removed. There is no exit stagger effect.
I want to have an individual element of the list animated out, and when the whole list is cleared, have them one by one animated out.
Full Repro in CodeSanbox
Parent Box
const variantsBoxContainer: Variants = {
hidden: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
staggerDirection: -1
}
},
show: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
staggerDirection: 1
}
}
};
let id = 3;
export const ParentBox = (props: ParentBoxProps) => {
const [items, setItems] = useState<Item[]>([
{ id: 1, text: "Test #1" },
{ id: 2, text: "Test #2" }
]);
return (
<motion.div
className="parentbox"
>
<button
onClick={() => {
id++;
setItems([...items, { id: id, text: `Click to delete id ${id}` }]);
}}
>
Add
</button>
<button
onClick={() => {
id++;
setItems([]);
}}
>
Remove All
</button>
<motion.ol
variants={variantsBoxContainer}
initial="hidden"
animate="show"
exit="hidden"
>
<AnimatePresence mode="popLayout">
{items
.sort((a, b) => a.id - b.id)
.map((d) => (
<Box
key={d.id}
data={d}
onRemove={(item) => {
const newList = items.filter((i) => i.id !== item.id);
console.log(newList);
setItems(newList);
}}
/>
))}
</AnimatePresence>
</motion.ol>
</motion.div>
);
};
Box
const variantBox: Variants = {
hidden: { opacity: 0, top: -100, transition: { duration: 2 } },
show: { opacity: 1, top: 0, transition: { duration: 2 } }
};
export const Box = (props: BoxProps) => {
return (
<motion.li
className="box"
variants={variantBox}
onClick={() => {
props.onRemove(props.data);
}}
>
{props.data.text}
</motion.li>
);
};
What I have tried so far:
Adding/Removing the explicit mention of initial, animate, exit on the Box component.
Adding/Removing the when option.
Tried all mode in the AnimatedPresence
Try to add a function for the hidden (exit) variant to have a custom delay per index
Ensure all Box all have unique key
Let me know if you have any idea what I am missing to have the animation on Box removal (children).
CodeSanbox
Exit animations will work if you explicitly indicate which variant to use for the animation states:
export const Box = (props: BoxProps) => {
return (
<motion.li
custom={props.index}
className="box"
variants={variantBox}
exit="hidden"
initial="hidden"
animate="show"
onClick={() => {
props.onRemove(props.data);
}}
>
{props.data.text}
</motion.li>
);
};
I believe AnimatePresence is conflicting with the staggerChildren prop since it appears between the parent and children. See this issue on GitHub.
Quickest workaround is probably to use dynamic variants and manually set a delay in the variants for the Box component (based on the index in the items array.

Passing div style as a function argument Javascript/ React

I'm building an application using React, framer-motion and react-intersection-observer.
Inside animation.js I have a function which is imported inside App.js and used as a component in App.js.
I want to apply an aspect ratio to some divs using style as a parameter, but it doesn't work.
<FadeAnimation name={'project-image--image'} style={'--ratio':16/9} />
Failed to set an indexed property on 'CSSStyleDeclaration': Indexed property setter is not supported.
I have other divs with this property and they are displayed correctly
<div className='project-image--image' style={{'--ratio':1/2}}/>
Animation.js
export const container = {
hidden: { opacity: 0, y: 5 },
visible: { opacity: 1, y: 0 }
}
function FadeAnimation({ children, name, delay, duration, style }) {
const controls = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
controls.start("visible");
}
}, [controls, inView]);
return (
<motion.div className={`${name}`} style={{`${style}`}}
ref={ref}
animate={controls}
initial="hidden"
transition={{ duration: duration, delay: delay }}
variants={{
visible: { opacity: 1, y: 0 },
hidden: { opacity: 0, y: 5 }
}}
>
{children}
</motion.div>
);
}
Have you tried:
<FadeAnimation name={'project-image--image'} style={{'--ratio':16/9}} />
(Adding another curly brace)
And then, in the FadeAnimationComponent using it as
<motion.div className={`${name}`} style={style} {/*...*/}/>

Watching for div width changes in vue

I have an nuxt app where I have two sidebars, one on the left and one on the right.
Both are fixed and body has padding from right and left.
In the middle I have <nuxt/> that loads pages.
Left sidebar can be minimized to 60px so I cannot use media queries for this and I need to watch for <nuxt/> width changes, in case that width is < 500px I would add some other classes. Something like media queries for element instead of viewport.
Is there a way to do this without additional javascript libraries, plugins etc..?
updated with two slider example:
A way to achieve the desired behavior will be to bind a dynamic width to your sliders and watch that width prop then bind the classes using the element ref upon changes :
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: "#app",
data() {
return {
silderWidth: {
first: '100',
second: '100'
}
}
},
computed: {
first() {
return this.silderWidth.first
},
second() {
return this.silderWidth.second
}
},
methods: {
toggleWidth(ele) {
this.silderWidth[ele] === '100' ?
this.silderWidth[ele] = "200" :
this.silderWidth[ele] = "100"
}
},
watch: {
first() {
this.$nextTick(() => {
this.silderWidth.first === '200' ?
this.$refs.silderWidth1.classList.add('background') :
this.$refs.silderWidth1.classList.remove('background')
})
},
second() {
this.$nextTick(() => {
this.silderWidth.second === '200' ?
this.$refs.silderWidth2.classList.add('background') :
this.$refs.silderWidth2.classList.remove('background')
})
}
}
})
.silder {
background-color: red;
height: 200px;
}
.silder--1 {
position: fixed;
left: 0;
top: 0;
}
.silder--2 {
position: fixed;
right: 0;
top: 0;
}
.background {
background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="silder silder--1" ref="silderWidth1" :style="{ width : first + 'px'}" #click="toggleWidth('first')"></div>
<div class="silder silder--2" ref="silderWidth2" :style="{ width : second + 'px'}" #click="toggleWidth('second')"></div>
</div>
Hope this helps:
window.onresize = () => {
let width = document.querySelector('nuxt').offsetWidth;
console.log(width);
};

How to appendTo React Components as simple as jQuery can

As I try to get into React I find I'm running through lists of things I can do very simply in jQuery but are an absolute nightmare in React.
https://codepen.io/PocketNinjaDesign/pen/boJoEd
have modified the goal from 2 elements to nth
The goal is that nth elements on the page can popup inside of any element / component I want on the page. I achieve this the good old fashion way by adding a data attribute containing Json with an array of components to appendTo.
You can also use JS to turn a component into a ninja and passing an object through containing the list of components to appear in.
$(function() {
$('.other-ninja').Ninja({
components: ['title', 'header', 'angry']
});
});
So, imagining these are React Components now, all different kinds of components, but the ninja block(s) can be told they can appear in any components they want.
How is that possible with React without going through a ball ache of declaration and indigestion?
Here is my code for the jQuery popup appendTo script as what I want to achieve in React.
Have altered the code since the answer I received as I don't think I
was putting my question across correctly.
HTML
<div class="ninja" data-ninja='{"components": ["title", "happy", "sad", "stinky", "header", "AnotherComponent"]}'></div>
<div class="mongoose" data-ninja='{"components": ["happy", "sad", "stinky", "angry", "footer"]}'></div>
<div class="other-ninja"></div>
<h1 class="title">Getting React to work like simple jQuery :-D</h1>
<p>Where ALL HTML elements on this page represent React Components. All components being
split into different files and imported using babel es6 compiler</p>
<div class="header">
<div class="angry"></div>
</div>
<div class="content">
<ul>
<li>Just a list showing more component depth</li>
<li>
<div class="someOtherComponent">
<div class="sad"></div>
</div>
</li>
<li>Trying to show the code works regardless of where the elements are</li>
</ul>
<div class="AnotherComponent">
<div class="SomeOtherComponent">
<div class="WhatAnotherComponent"><div class="happy"></div></div>
</div>
</div>
</div>
<div class="footer">
<div class="whatever">
<div class="stinky"></div>
</div>
</div>
SCSS
Removed the SCSS as it was detracting from the main focus of markup and javascript
Script
$(function() {
function Ninja(e, options) {
var $this = $(e);
options = $.extend({}, $this.data('ninja'), options);
var componentList = options.components;
setInterval(function() {
var randomNum = Math.floor(Math.random() * componentList.length);
$this.appendTo('.' + componentList[randomNum]);
}, 1000);
}
$.fn.Ninja = function(options) {
$(this).each(function(i, e) {
Ninja(e, options);
});
};
$('[data-ninja]').each(function(i, e) {
$(e).Ninja();
});
});
$(function() {
$('.other-ninja').Ninja({
components: ['title', 'header', 'angry']
});
});
React might seem like "nightmare" when you used to use jQuery but actually using React is really easy. You just need to adjust your coding logic to suit React guidelines.
The code you shared can be implemented in React many different ways. I did a small working version for you to compare. At first-look it might seem a lot more code to achieve same simple affect but React has a lot more control over the DOM (Virtual DOM) and can be manipulated a lot more different ways. Most of the code in this example is same for all components. You just need to put consideration render method. Appending or removing a component/element can be achieved by just a simple if statement. Also component logic gives you a big flexibility and re-usability. There are a lot of good examples and tutorials online that can show you how you can solve thing in react way. A really good start point is React Docs and awesome-react.
import React from 'react';
import { render } from 'react-dom';
const styles = {
component: {
position: 'absolute',
width: 100,
height: 100
},
happy: {
position: 'absolute',
width: 100,
height: 100,
top: 100,
left: 40,
backgroundColor: 'yellow'
},
sad: {
position: 'absolute',
width: 100,
height: 100,
top: 20,
left: 180,
backgroundColor: 'blue'
},
angry: {
position: 'absolute',
width: 100,
height: 100,
top: 160,
left: 500,
backgroundColor: 'red',
},
stinky: {
position: 'absolute',
width: 100,
height: 100,
top: 210,
left: 300,
backgroundColor: 'green'
},
ninja: {
position: 'absolute',
zIndex: 100,
width: 50,
height: 50,
backgroundColor: '#000'
},
deathstar: {
position: 'absolute',
zIndex: 100,
width: 20,
height: 20,
backgroundColor: '#444'
}
};
const Ninja = () => (<div style={styles.ninja}></div>);
const DeathStar = () => (<div style={styles.deathstar}></div>);
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
types: ['happy', 'sad', 'angry', 'stinky'],
ninja: 0,
deathstar: 0
};
this.intervalNinja = null;
this.intervalDeathstart = null;
}
randomNumber() {
return Math.floor(Math.random() * (3 - 0 + 1)) + 0;
}
componentWillUnmount() {
clearInterval(this.intervalNinja);
clearInterval(this.intervalDeathstart);
this.intervalNinja = null;
this.intervalDeathstart = null;
}
componentDidMount() {
this.intervalNinja = setInterval(() => {
this.setState({
ninja: this.randomNumber()
});
}, 1082);
this.intervalDeathstart = setInterval(() => {
this.setState({
deathstar: this.randomNumber()
});
}, 987);
}
render() {
return(
<div>
{this.state.types.map((type, index) => (
<div style={styles[type]}>
{this.state.ninja === index && <Ninja />}
{this.state.deathstar === index && <DeathStar />}
</div>
))}
</div>
)
}
}
render(<App />, document.getElementById('root'));

React - change state right after previous state change was rendered

I wanna make cool box grow animation (expand) when user clicks on it and I want to do it following way:
user clicks on expand button -> get div dimensions and top/left positions via ref, store it in state and assign div's style to these values
changed expanded state variable and change div's position to fixed, also change left, top values and width, height css values
My problem is in initial div expand click. It seems that both state's changes are rendered in one cycle so I don't see smooth animation on first expand click. I've tried to do it via setState callback, also tried to update expanded in componentDidUpdate method once div dimensions are in state, nothing worked except delaying expanded set via setTimeout.
Code example via setState callbacks
if (chartsExpanded.get(chart) === "collapsed-end" || !chartsExpanded.get(chart)) {
this.setState({
chartsProportions: chartsProportions.set(
chart,
Map({
left: chartProportions.left,
top: chartProportions.top,
width: chartProportions.width,
height: chartProportions.height
})
)
}, () => {
this.setState({
chartsExpanded: chartsExpanded.set(chart, "expanded")
})
})
}
...
<div
className={`box customers-per-sources-count ${
customersPerSourcesCount.loading ? "loading" : ""
} ${
chartsExpanded.get("customersPerSourcesCount")
? chartsExpanded.get("customersPerSourcesCount")
: "collapsed-end"
}`}
ref={el => {
this.chartRefs["customersPerSourcesCount"] = el
}}
style={{
left: chartsProportions.getIn(["customersPerSourcesCount", "left"], "auto"),
top: chartsProportions.getIn(["customersPerSourcesCount", "top"], "auto"),
width: chartsProportions.getIn(["customersPerSourcesCount", "width"], "100%"),
height: chartsProportions.getIn(["customersPerSourcesCount", "height"], "100%")
}}
>
How can I achieve that style from chartsProportions will be rendered before class based on expanded value is changed? I don't want to use setTimeout nor want to update all charts proportions onScroll event etc.
You just need to pass setState a function instead of an object, to ensure the state changes are applied in order:
this.setState(previousState => ({
previousChange: "value"
}))
this.setState(previousState => ({
afterPreviousChange: previousState.previousChange
}))
https://reactjs.org/docs/react-component.html#setstate
Another option might be to pass a callback to setState that runs after the state changes have been applied, like:
this.setState({ someChange: "value" }, () => this.setState({
otherChange: "value"
}))
CSS transitions could help with this too.
Using React state to animate properties is not the right way to do it. State updates will always get batched and you generally don't want to re-render your entire component 60 times per second
Store 'expanded' boolean in your state, and change element's class accordingly. Use css to add animations between two states
handleClick = () => {
this.setState({ expanded: !this.state.expanded })
}
render() {
return (
<div
className={`box ${this.state.expanded ? 'expanded' : ''}`}
onCLick={this.handleClick}
/>
)
}
in your css
.box {
width: 100px;
height: 100px;
transition: width 2s;
}
.expanded {
width: 300px;
}
added based on comments:
What you want to do is:
set position: fixed to your element. This would snap it to the top of the screen instantly, so you need to pick the right top and left values so that position fixed starts off where it was when position was static (default). For that you can use element.getBoundingClientRect()
calculate desired top and left attributes that would make your element appear in the middle of a screen, and apply them
very important: between step 1 and 2 browser has to render the page to apply position and initial top and left values, in order to have something to start animation from. It won't be able to do that if we apply both of these styles synchronously one after another, as page will not render until JS stack frame is clear. Wrap stage 2 logic in setTimeout which will make sure that browser renders at least once with styles applied at stage 1
rough working example:
class Example extends React.Component {
constructor(props) {
super(props)
this.state = {
expanded: false,
style: {}
}
}
handleClick = (e) => {
if (!this.state.expanded) {
const r = e.target.getBoundingClientRect()
const style = {
top: r.y,
left: r.x,
}
this.setState({
expanded: !this.state.expanded,
style
})
setTimeout(() => {
this.setState({
style: {
top: (window.innerHeight / 2) - 50,
left: (window.innerWidth / 2) - 50,
}
})
})
} else {
this.setState({
expanded: false,
style: {}
})
}
}
render() {
return (
<div className={'container'}>
<div className={'empty'} />
<div className={'empty'} />
<div className={'empty'} />
<div
onClick={this.handleClick}
className={`box ${this.state.expanded ? 'expanded' : ''}`}
style={this.state.style}
/>
</div>
)
}
}
and styles.css
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.container {
height: 200vh;
}
.empty {
height: 100px;
width: 100px;
border: 1px solid gray;
}
.box {
height: 100px;
width: 100px;
border: 3px solid red;
transition: all 0.5s;
}
.expanded {
position: fixed;
}

Categories

Resources