I am trying to create an animation for moving a child element from one parent element to another using React.
A user should be able to click on an element and see it move into another div.
I made a simple demo component (without the animation) to show what I mean. When an element is clicked, the state updates and the elements are re-rendered in the correct place.
class App extends React.Component {
state = {
list: ['Alice', 'Bob', 'Charlie', 'David', 'Emily', 'Frank'],
top: [0, 1, 2],
bottom: [3, 4, 5]
}
moveDown = (item) => {
let { top, bottom } = this.state
this.setState({
top: top.filter(x => x !== item),
bottom: [...bottom, item]
})
}
moveUp = (item) => {
let { top, bottom } = this.state
this.setState({
top: [...top, item],
bottom: bottom.filter(x => x !== item)
})
}
render() {
let { top, bottom, list } = this.state
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
height: '90vh',
width: '100%'
}}>
<div>
{top.map((item) =>
<div
onClick={() => this.moveDown(item)}
style={{color:'red'}}>{list[item]}</div>
)}
</div>
<div>
{bottom.map((item) =>
<div
onClick={() => this.moveUp(item)}
style={{color:'green'}}>{list[item]}</div>
)}
</div>
</div>
)
}
}
Codepen demo: https://codepen.io/ee92/pen/LqrBjL?editors=0010
Big appreciation to and thanks in advance for any help or advice on how to achieve this div-to-div animation.
No it's not possible
It's not possible to animate in that way because the DOM thinks you're removing a div and then adding a new div. Even though it's the same div to you, the DOM doesn't have that context. Animations are controlled by changes to CSS, not HTML.
...but here's how to do it
If you actually need both lists to stay in different divs the best you can do is either:
Animate the old item to the new item position, then delete the old item and show the new item.
Remove the old item and create a new item where the old item was and move it to the new item position.
Same concept, two ways of doing it.
I modified your existing sample to show a simplified version of option 2. Note that there are a number of animation decisions to make like what happens when the list gets smaller, how should the items change from red to green, etc., and I didn't try and objectively solve them. Also, this would be much easier if you could have all the items for both lists in one div, and control their positions absolutely. But if they need to end up in separate divs...
https://codepen.io/sallf/pen/VgBwQr?editors=0010
What's going on
Adding a transition to .item we can make the animation happen when we make adjustments to the transform property.
On item click we update our lists in state and add...
transition.item to know which item is animating...
transition.startTop to know the offset y position the item should start at relative to the bottom of the list it's moving to, and...
transition.startAnim as a flag to control the animation.
Since transitions need something to change before they'll animate, we use setTimeout to delay the change of transition.startAnim which basically causes the animation from the computed position, back to 0.
Related
I have Note components that are rendered using notes.map(), each with position: static. The notes are draggable using react-draggable npm module.
The functionality I'm trying to achieve is when a note is deleted, to not affect the position of notes that have been dragged by the user. I've attempted to set position: absolute for notes that have been dragged. However, this causes the note to 'jump' in position once (happens when removed from flow).
-- Initial State:
-- After first drag attempt, test note jumps on top of other note:
-- Able to drag normally after first attempt:
I've included relevant code for Notes.jsx component:
function Note(props) {
const [dragDisabled, setDragDisabled] = useState(true);
const [beenDragged, setBeenDragged] = useState(props.beenDragged);
const [position, setPosition] = useState({ xPos: props.xPos, yPos: props.yPos });
// Drag Note Functions
function handleClick() {
setDragDisabled(prevValue => {
return !prevValue;
})
}
function firstDrag(event) {
if (!beenDragged) {
axios.post("api/note/beenDragged", {id: props.id})
.then(setBeenDragged(true));
}
}
function finishDrag(event, data) {
setPosition({ xPos: data.x, yPos: data.y });
}
useEffect(() => {
axios.post("/api/note/updateposition", {position, id: props.id });
}, [position]);
return <Draggable
disabled={dragDisabled}
onStart={firstDrag}
onStop={finishDrag}
defaultPosition={{ x: props.xPos, y: props.yPos }}
// position={location}
>
<div className='note' style={{position: beenDragged ? 'absolute' : 'static'}}>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleClick}>
{dragDisabled ? <LockIcon /> : <LockOpenIcon />}
</button>
<EditPopup title={props.title} content={props.content} editNote={editNote} />
<DeletePopup deleteNote={deleteNote} />
</div>
</Draggable>
}
and for my CSS styling
.note {
background: #fff;
/* background-image: url("https://www.transparenttextures.com/patterns/notebook-dark.png"); */
background-image: url("https://www.transparenttextures.com/patterns/lined-paper-2.png");
border-radius: 7px;
box-shadow: 0 2px 5px rgb(120, 150, 179);
padding: 10px;
width: 240px;
margin: 16px;
float: left;
}
Relevant code for how App.jsx renders the notes:
function App() {
const [notes, setNotes] = useState([]);
return (
<div id="bootstrap-override">
{notes.map((note) => {
return <Note
key={note._id}
id={note._id}
title={note.title}
content={note.content}
xPos={note.xPos}
yPos={note.yPos}
beenDragged={note.beenDragged}
deleteNote={deleteNote}
editNote={editNote}
/>
})}
</div>);
}
Any help is much appreciated, thank you!
I solved the issue not directly, but rather using a different approach to get the result I was looking for.
To summarize, I used position: absolute for all notes, and created them at random x,y coordinates within a boundary. This way deleted notes would not affect the position of existing notes.
Hope this helps anyone with similar issues!
When your draggables are position: static, you'll experience that screen jank when removing an element because the order of the elements in the document help define where your draggables live, regardless of their dragged state (which boils down to something like transform: translate(100px, 150px);.
The translate is like saying: "go over 100px and up 100px from wherever you live. When we delete the first element, everybody shifts. They can still translate(100px, 150px); but the point from which they originate will have changed.
Someone mentioned setting the draggable's to position: absolute and the parent as position: relative. That works, but might introduce headaches of it's own.
I will offer an innocent, perhaps naive, solution: hiding the element. A quick and dirty way: instead of removing the element from the DOM, you can set the element's visibility: none. You can even delete the item in the backend and offer a "restore note" feature in the UI since the draggable will still contain content.
And whenever you're hiding things, make sure you do so with accessibility in mind. Here's a good article on that.
I also made a derpy screencast: https://www.youtube.com/watch?v=Fr0PfT3Frzk&ab_channel=SamKendrick. It goes a little fast, but I begin by deleting the first element to illustrate your problem. Then I undo the delete and instead attach a visibility: hidden style to the element I want to delete.
I have a HTML5 Video panel in my app. I managed to make a progressbar which is working perfectly. But I also want to ability to change currentTime of the video by clicking to any position on progressbar.
How can I get current position as ratio using React? I searched progressbar components on google but it seems none of them give value of clicked position but just display.
Or simply, when user clicks on different position of the progressbar, I want to change/get value of progressbar to that position.
Update:
I have updated demo and it seems progressbar gone crazy when click multiple times and it seems not close to the clicked position either.
Demo: https://stackblitz.com/edit/react-ts-zudlbe?file=index.tsx
This is the whole component I tested it on.
Seems to work
const ProgressBar = ({ duration, progress }: ProgressProps) => {
return (
<div
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
// duration is the video duration. For example 152.
// How can i catch the % (ratio) of clicked position of this progrssbar?
// then I will send this % (lets say 5) to the seek function like:
const x = (event.clientX * duration) / event.currentTarget.offsetWidth; // how to get sec?
callback(x);
}}
style={{
backgroundColor: "#ddd",
borderRadius: 3,
flex: 1,
height: 6,
overflow: "hidden",
position: "relative"
}}
>
<Progress progress={progress} />
</div>
);
};
I wanted to know if there is a way to readjust tabs underneath a snackbar when it pops up at the top of the page? Like let's say tabs are at the very top of the page, snackbar with a height of 10px pops up at the top of the page then I want the snackbar to be the top element and the tabs to move underneath it when it is shown without them overlapping on each other?
Here's a stackblitz that might do what you want
https://stackblitz.com/edit/angular-f9k7ev
Basically what I'm doing is triggering some code (button, event, whatever) to set my snackbar to open up top, and adding a class to our container (here, a div, for you, your tabs) to move them down by an arbitrary value (here we do 10em) using CSS. We also listen to our afterDismissed() subscription to come back and then remove that class.
constructor(private _snackBar: MatSnackBar) {}
openSnackBar() {
const ourSnackbar = this._snackBar.openFromComponent(PizzaPartyComponent, {
duration: this.durationInSeconds * 1000,
verticalPosition: 'top'
});
ourSnackbar.afterDismissed().subscribe(() => this.removeOurClass());
let elements = document.getElementsByTagName("div");
Array.from(elements).forEach(element => {
element.classList.add('moved');
});
}
removeOurClass() {
let elements = document.getElementsByTagName("div");
Array.from(elements).forEach(element => {
element.classList.remove('moved');
});
}
and our CSS
.moved {
margin-top: 10em;
}
and the HTML we're moving (not the snackbar), the div we look for.
<div class="mat-app-background basic-container">
<snack-bar-component-example>loading</snack-bar-component-example>
</div>
I been trying to make a Masonry gallery with a sequential fade-in effect so that the pictures fade in one by one. And there is also a shuffle feature which will randomize the images and they fade in again after being shuffled.
here is the demo and the code:
https://tuo1t.csb.app/
https://codesandbox.io/s/objective-swartz-tuo1t
When first visiting the page, the animation is correct. However once we click on the shuffle button, something weird happened: There are often some pictures don't fade-in sequentially after the image before them faded in, there is even no fade-in animation on them, they just show up out of order.
The way I achieved this animation is by adding a delay transition based on the index of the image, and use ref to track images.
first I initialize the ref
let refs = {};
for (let i = 0; i < images.length; i++) {
refs[i] = useRef(null);
}
and I render the gallery
<Mansory gap={"1em"} minWidth={minWidth}>
{imgs.map((img, i) => {
return (
<PicContainer
index={img.index}
selected={isSelected}
key={img.index}
>
<Enlarger
src={img.url}
index={img.index}
setIsSelected={setIsSelected}
onLoad={() => {
refs[i].current.toggleOpacity(1); <--- start with zero opacity images till those are loaded
}}
ref={refs[i]}
realIndex={i}
/>
</PicContainer>
);
})}
</Mansory>
for the every image component
class ZoomImg extends React.Component {
state = { zoomed: false, opacity: 0 };
toggleOpacity = o => {
console.log("here");
this.setState({ opacity: o }); <-- a setter function to change the opacity state via refs:
};
render() {
const {
realIndex,
index,
src,
enlargedSrc,
setIsSelected,
onLoad
} = this.props;
return (
<div style={{ margin: "0.25rem" }} onLoad={onLoad}>
<Image
style={{
opacity: this.state.opacity,
transition: "opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94)",
transitionDelay: `${realIndex * 0.1}s` <--- add a delay transition based on the index of the image.
}}
zoomed={this.state.zoomed}
src={src}
enlargedSrc={enlargedSrc}
onClick={() => {
this.setState({ zoomed: true });
setIsSelected(index);
}}
onRequestClose={() => {
this.setState({ zoomed: false });
setIsSelected(null);
}}
renderLoading={
<div
style={{
position: "absolute",
top: "50%",
color: "white",
left: "50%",
transform: "translateY(-50%} translateX(-50%)"
}}
>
Loading!
</div>
}
/>
</div>
);
}
}
I used console.log("here"); in the setter function, which will be called for changing the opacity state via refs. There are 16 images, so initially it is called 16 times. But when I clicked on the shuffle button, you can see that it is called fewer than 16 times because some of the pictures show up directly without fading in.
I been struggling with this problem for days and really hope someone can give me some hints.
The problem is that your are adding only some new images in the shuffle method, one approach is to apply 0 opacity to all refs first, then wait a few ms to add 1 opacity again, like here.
But, I would recommend a better approach for animation, I love shifty and its Tweenable module.
I have a panel in which the keyboard is always up since I do not wish the users to dismiss it. In that panel I have a FlatList which looks like this:
<KeyboardAvoidingView style={{ flex: 1 }}>
<FlatList
// This keeps the keyboard up and disables the user's ability to hide it.
keyboardShouldPersistTaps="handled"
data={this.state.examples}
keyExtractor={(item, index) => index.toString()}
renderItem={this._renderItem}
contentContainerStyle={{ flex: 1}}
/>
</KeyboardAvoidingView>
So far so good, I have achieved what I wanted. However, when the keyboard is up - it hides the bottom part of the items rendered by the FlatList. And users cannot scroll up and view the last items because they stay behind the keyboard.
How can I preserve the Keyboard opened (and disable the ability to be dismissed) whilst being able to view and scroll through the whole content of the FlatList?
You can add a keyboard listener event to get the height of the keyboard.
this.keyboardWillShowListener = Keyboard.addListener('keyboardWillShow', (e) => {
this.setState({ keyboardHeight: e.endCoordinates.height, keyboardShow: true })
Animated.timing(this.visibleHeight, {
duration: e.duration,
toValue: 1,
easing: Easing.inOut(Easing.ease)
}).start()
})
View code like this
<Animated.View style={Platform.OS === 'android' ? { flex: 1 } : {
height: this.visibleHeight.interpolate({
inputRange: [0, 1],
outputRange: [height - this.NavHeaderHeight, height - this.state.keyboardHeight - this.NavHeaderHeight]
})
}
} >
/*Your FlatList*/
</Animated.View>
I hope it works for you
I've been to a similar situation. I had a bottom Floating Action Button at the lower right corner, hiding the last item a bit.
So, I added a fake blank item to the end of the list so that I could scroll it up a bit more.
It's simple and tricky. I hope it works for you as well, if you add a few blank itens or one wide enough blank item.
EDIT 1:
Suppose your data array is something like this: [{title: "Item 1"}, {title: "Item 2"}]
You have to concat the new blank item to the data array while passing it to the <FlatList>, like this:
<FlatList
keyboardShouldPersistTaps="handled"
data={this.state.examples.concat({title:"\n\n\n\n\n"})}
keyExtractor={(item, index) => index.toString()}
renderItem={this._renderItem}
contentContainerStyle={{ flex: 1}}/>
Adjust the amount of "\n" until you can scroll the list to be visible. There must be a minimum amount. And make sure your _renderItem don't set the item hight to a fixed value.