Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
UnioninDesign

Troubles with range slider updating progress() method for timelineMax in React?

Recommended Posts

Hello Community - my second post as I'm very stuck trying to integrate a range-slider in React that will be friends with the progress() method to move through the timeline! I can pass around the value I need to my slider, but getting undefined errors when I try to pass this over to this.tl.progress(this.state.value) etc?

 

I've read through several posts, but none look very current...in fact most are going back 2-3 years and the react-way has changed! Looking through the 'getting started with react post from Rodrigo' there are some useful hints, but it seems overly complex, compared to how easy it was to plugin the play, pause, reverse and restart methods into a button with an onClick event handler. Do we really need a separate state management file to pass down the state as props to a child component just to get the slider to move through the timeline without breaking?

 

I also got some very good support already (This gives you more of an idea what I am working on...sorry for the lack of a reduced codepen example as this is a large full-stack application! coming soon I promise!)

 

Here's some code of how I got the play, pause, reverse, and restart methods working...these plugged right in! (FYI ButtonGroup and Button components are from React-Bootstrap):

<Row>
                                    <Col md={12}>
                                        <ButtonGroup className="animationControls">
                                            <Button bsStyle="primary" onClick={() => this.tl.play()}>
                                                <Glyphicon glyph="play" />
                                                {"Play"}
                                            </Button>
                                            <Button bsStyle="primary" onClick={() => this.tl.pause()}>
                                                <Glyphicon glyph="pause" />
                                                {"Pause"}
                                            </Button>
                                            <Button
                                                bsStyle="primary"
                                                onClick={() => this.tl.reverse()}>
                                                <Glyphicon glyph="backward" /> {" Reverse"}
                                            </Button>
                                            <Button
                                                bsStyle="primary"
                                                onClick={() => this.tl.restart()}>
                                                <Glyphicon glyph="step-backward" />
                                                {"Restart"}
                                            </Button>
                                        </ButtonGroup>
                                    </Col>
                                </Row>

 

Until bootstrap-4 is up and running with react, you do not have range slider in your form components, so I had to look elsewhere. After trying a few different versions, the npm package rc-slider seems to be the most lightweight (little to no boilerplate required!)

 

create your styles for the slider before your class function:

 

const railStyle = {
    position: "relative",
    width: "90%",
    margin: "0% 0% 0% 3%",
    height: 10,
    borderRadius: 7,
    cursor: "pointer",
    backgroundColor: "#afafaf"
};
const handleStyle = {
    height: 15,
    width: 15,
    backgroundColor: "white",
    borderTopLeftRadius: 10,
    borderTopRightRadius: 10,
    border: "3px solid #E5F1FC",
    top: -2,
    position: "absolute"
};

 

AND be sure to set your starting value in the constructor....probably 0 since that would be the start of your timeline....

constructor(props) {
        super(props);
        this.tl = new TimelineMax();
        this.state = {
            center: [46.8, 8.3],
            zoom: 1,
            value: 0
        };
        this.handleZoomIn = this.handleZoomIn.bind(this);
        this.handleZoomOut = this.handleZoomOut.bind(this);
        this.handleReset = this.handleReset.bind(this);
        this.handleSliderChange = this.handleSliderChange.bind(this);
    }

 

Next, I have two functions...please note that onSliderChange and onAfterChange are pre-built methods for the react rc-slider component. these successfuly track and log the value as you drag along the timeline, but kill the animation! 

 

    onSliderChange = value => {
        this.setState({ value });
        console.log("Value is: ", value);
    };
    onAfterChange = value => {
        console.log(value);
        this.tl.progress(value / 100);
    };
 

 

.....And lastly, the slider component itself, inside render()

 

   <Slider
                                            className="slider"
                                            style={railStyle}
                                            handleStyle={handleStyle}
                                            min={0}
                                            max={bookingData.length}
                                            value={this.state.value}
                                            onChange={this.onSliderChange}
                                            //onInput={this.handleSliderChange}
                                            onAfterChange={this.onAfterChange}
                                        />

 

I know this may be hard to digest without a working example. I'll try to make a reduced case, but here's the issue...inside the Slider component, if you drag the slider around, it will successfully log the value I need. Where can I pass the value to this.tl.progress(this.state.value / 100) etc to get the timeline to respond? I've tried a dozen different ways, and I either get that value is undefined, or when I try to pass this in to the onSliderChange I get my fav error about expected a function, but instead saw an expression, no unused expressions. dragging the slider around kills the timeline, or depending where I do it, will make the animated elements disappear from the screen. Grrrrr! React is very powerful, but the need to constantly update the state of components during their lifecycle make these kinds of things very frustrating! If anyone has solved this or can send a link to an example of how to do this it would be greatly appreciated! If I figure it out on my own I will update the post - I know I'm close!

 

Thanks community!

 

 

 

Link to post
Share on other sites

Hi,

 

I know the pain of things getting a bit convoluted sometimes for something that doesn't seem too complicated, but believe me, as soon as you start working on your own re-usable components, you'll find yourself extending their API to accommodate all the uses you'll give them in your apps.

 

Back on topic the one thing I noticed is this:

 

// here you pass as max bookingData.length
<Slider
  className="slider"
  style={railStyle}
  handleStyle={handleStyle}
  min={0}
  max={bookingData.length}
  value={this.state.value}
  onChange={this.onSliderChange}
  //onInput={this.handleSliderChange}
  onAfterChange={this.onAfterChange}
/>

 

The max value of the slider will be whatever length bookingData will have (I'll assume an array). Is this value 100 or is it a dynamic value?. Because you're calculating the progress value using 100:

 

onAfterChange = value => {
  console.log(value);
  this.tl.progress(value / 100);
};

 

So for example if the length of booking data is 25, at it's maximum point the value returned by the slider will be 25, but since you're dividing by 100, the progress of the GSAP instance will be 0.25, that is one quarter of the animation; see what I mean?

 

So instead of dividing by 100 divide by the length of booking data:

 

onAfterChange = value => {
  console.log(value);
  this.tl.progress(value / bookingData.length);
};

 

Give that a try and let us know how it goes.

 

Happy tweening!!

  • Like 3
Link to post
Share on other sites

Rodrigo - the man I was hoping to hear from! I've spent LOTS of time in this forum the past week or so, and your posts are always very helpful! Issue fixed per below! But the next one at the bottom of this post - how do we get the slider to update the position based on the current value of the animation?

 

BTW - Good eye spotting an inconsistency in my code! Easy to do when you try things a bunch of different ways...however, when setting a range of 0 to 100, or 0 to bookingData.length (which indeed gives you a value for how long the dataset is...I'll stick with that!)....the same result was that it kills the animation, but not getting any errors in the console. You could continue to drag the slider around and it will keep logging the values, but the play, pause, resume don't function, and you need to refresh the page to get the animation to run again.

 

After a few more minutes playing around...as I'm pretty sure I had tried what you had suggested, but with no luck...I noticed some code I had copied into to the top of the render method:

    render() {
        this.tl
            .kill()
            .clear()
            .pause(0);
        return (

 

The fix was to remove the .kill() and .clear() methods!!!!! Although I'm a little curious why these are here and what purpose they might serve, perhaps to pass in some kind of initial state to the playback methods at rendering...which is not best practice, correct? better to set the initial state in your constructor, or even to use a parent level component to manage the state and pass it down as props? Still have lots to learn!

 

For posterity, scrolling on the timeline now brings you to the next value (or booking!), but it does help to pass in the .resume() method to get the timeline working automatically, if not it will be pause when onAfterChange talks to the slider:

    onAfterChange = value => {
        console.log(value);
        this.tl.progress(value / bookingData.length);
        this.tl.resume();
    };

 

So the next challenge, which I hope to figure out soon...we need the range slider to update itself as the timeline progresses. It sounds like what we need is an onUpdate callback:

    onSliderChange = value => {
        this.setState({ value });
        console.log("Value is: ", value);
    };
    onAfterChange = value => {
        console.log(value);
        this.tl.progress(value / bookingData.length);
        this.tl.resume();
    };
    updateSlider = () => {
        let currentProg = this.state.value;
        this.tl.progress(currentProg * bookingData.length);
        console.log("running", currentProg);
    };

 

I keep getting an error that on updateSlider is undefined? I am trying to pass it in like so.  Quick note that all of my animations are currently housed in componentDidMount, but I do need to move these to an external animation.js file...

 

onSliderChange = value => {
        this.setState({ value });
        console.log("Value is: ", value);
    };
    onAfterChange = value => {
        console.log(value);
        this.tl.progress(value / bookingData.length);
        this.tl.resume();
    };
    updateSlider = () => {
        let currentProg = this.state.value;
        this.tl.progress(currentProg * bookingData.length);
        console.log("running", currentProg);
    };
 
    componentDidMount() {
        bookingInterval();
 
        //Animations with GSAP
 
        this.tl = new TimelineMax(); // parameter: { "onUpdate", updateSlider }
 
        var next = 3;
        this.tl
            .staggerFrom(
                ".animatedTick",
                3,
                {
                    rotationY: -180,
                    ease: SlowMo.easeOut,
                    y: 700,
 
                    delay: 1,
                    scale: 2,
                    opacity: 0
                },
                3,
                "nextBooking -=3"
            )
            .staggerFrom(
                ".custMarkers",
                1.5,
                {
                    cycle: { x: [-1000, 1000], y: [-1000, 1000] },
                    bezier: {
                        curviness: 1,
                        values: [{ x: 0, y: 0 }, { x: 125, y: -80 }, { x: 250, y: 0 }]
                    },
                    ease: Power1.easeOut,
                    opacity: 0,
                    scale: 10
                },
                next,
                1
            )
            .staggerFrom(
                ".clientMarkers",
                1.5,
                {
                    cycle: { x: [-1000, 1000], y: [-1000, 1000] },
                    bezier: {
                        curviness: 1,
                        values: [{ x: 0, y: 0 }, { x: 125, y: -80 }, { x: 250, y: 0 }]
                    },
                    ease: Power1.easeOut,
                    opacity: 0,
                    scale: 7
                },
                next,
                1.5
            )
            .staggerFrom(
                ".lineAmation",
                2.1,
                {
                    ease: Sine.easeIn,
                    opacity: 0
                },
                next,
                2,
                "-=1"
            );
    }

    render() {
        this.tl.pause(0);
        return (............

 

So up next, where do I pass in this callback?

 

Thanks a ton!

Link to post
Share on other sites

Quick update...so here's a snip of the callback function:

function updateSlider(context) {
			console.log("State? ", context.state);
			let currentProg = context.state.value;
			//context.setState({ value: currentProg + 1 });
			context.tl.progress(currentProg / bookingData.length);
		}

 

And I'm calling it at the timeline declaration at so, which I believe is correct:

	this.tl = new TimelineMax({
			onUpdate: updateSlider,
			onUpdateParams: "{self}"
		
		});

 

But...because all of the animations live inside componentDidMount(){}, if I pass this in at after the timeline, inside componentDidMount...we are not getting passed the value that has been set here inside these two slider methods:


	onSliderChange = value => {
		this.setState({ value });
		console.log("Value is: ", value);
	};
	onAfterChange = value => {
		console.log(value);
		this.tl.progress(value / bookingData.length);
		this.tl.resume();
	};

...It will continue to update with no value and error out after a few seconds?

 

Alternatively, passing in the updateSlider callback right after onSliderChange and onAfterChange, just above componentDidMount, will give you an undefined error (makes sense). I think this is not a GSAP problem! but something to do with the architecture or my app! Any ideas are appreciated! Sooooo close!

 

Link to post
Share on other sites

Hi,

 

There is a lot to digest from your posts. For what I can understand (or I'm understanding), you want the slider to update the progress of the Timeline instance and the Timeline to update the slider value as well.

 

So, when the app starts, the timeline is moving forward and updating the slider, but when the user interacts with the slider, the timeline should be paused and it should be updated based on the value of the Timeline's progress. Am I getting everything right? If so, does this works as you expect?:

 

https://stackblitz.com/edit/react-rc-slider-gsap?file=index.js

  • Like 3
Link to post
Share on other sites

Ah ha - as we say in French - "le mot juste!" Rodrigo you sir are a Wizard!

 

I got pretty close by the end of the day yesterday - I think it came down to missing a 'this' in front of my updateSlider callback! (it's a class...duh...). Now onto the next...I'm in awe of GSAP and the support from its community. There are some very exciting possibilities for making data visualizations here!

 

My apologies again that I have not supplied any codepen or reduced case examples...since this is a worky-thing it's not very public...but I will try to replicate something with a more public dataset and will update this post...maybe this weekend? If anyone is interested in building interactive maps using D3 (in this case react-simple-maps) along with React, and a robust animation library like GSAP to create an interactive timeline, this is the place to be!

 

Thanks again!

  • Like 2
Link to post
Share on other sites

Hello again!

 

I took some time to make a test case to play around with...getting stuck on a few things...

 

Can we talk about use of createRef, callback refs etc? I noticed in the example link you sent, your target looks like this:

div className="box" ref={e => this.box = e}>
<span>
MOVE & ROTATE
</span>
</div>

 

I tried to replicate something similar, but could be getting stuck with a 3rd party component library so I can play with maps (react-simple-maps, that is...)

 

Here's kicker - I've had GSAP working for several weeks now with test data that was hard coded. When trying to make my app iterative, as discussed in the posts above, I'm trying to go through the best practices of fetching data in a parent component, passing state down as props to a child component - my data renders, but the animations are gone?

 

Up until then, my animations were working successfully by adding a className to the html element (in this case, the svg circle tag inside the <Marker> 3rd party component)

 

I've tried to recreate a similar use case, at the moment I'm not getting any animations to fire up?

https://stackblitz.com/edit/react-globe-timeline?embed=1&amp;file=index.js

 

Let me know if you have any suggestions...much appreciated!

Link to post
Share on other sites

Hi,

 

I need more specific information about this.

 

What exactly are you trying to animate?

 

Then in your code I see this:

 

.staggerFrom("animateMapMarker", 5, 
  {autoAlpha: 1, ease: Elastic.easOut, scale: 5, yoyo: true, repeat:1},
next);

 

Here you're passing a string and not an array, nor a selector that GSAP can work with, if you change that it kind of works:

 

.staggerFrom(".animateMapMarker", 5, 
  {autoAlpha: 1, ease: Elastic.easOut, scale: 5, yoyo: true, repeat:1},
next);

 

Now for the looks of it, you're stumbling upon something you just can't control. You're using a third party component and you need to reach the DOM nodes created by that component. For security reasons that normally is not possible unless the component's creator adds something to the component's API that allows users to set a reference to the DOM nodes. Right now you're using a class selector for that and, while it works, as you mentioned is not the best practice. My advice is this: contact the component's creator to see if there is a way to actually achieve that or fork the component and bake your own solution to reach the DOM nodes. If neither one of those alternatives is viable, then I'd suggest to write some code, perhaps a method in order to check if the selected elements are actually present in the DOM and to update both the selection array and the timeline instance, if the DOM is updated from either the server or a user interaction.

 

Happy Tweening!!

  • Like 2
Link to post
Share on other sites

Thanks Rodrigo! You've got the right idea...and it was getting late...I was missing (duh) the . in front of the className "animateMapMarker" which should have been ".animateMapMarker", like you said GSAP was getting a string for the target and not the element with a className of ".animateMapMarker". And yes, having added the . it kind of works...There's a method in the map library that will move the 'center' of the map and rotate the globe, so I do need to write some more code to get it to a point where,  when I call the staggerFrom method, the map markers get bigger (which works) AND that marker would then be displayed front and center! If I could get a bezier line to animate and connect the dots, this could be cool app that would track movements around the globe with an interactive timeline...but it's not quite there yet.

 

I'll see what I can do to use this example to recreate the problem I am having at work...I tried it again this morning, but how weird! Data that was hard-coded in an external file, then imported and passed to the 3rd party components would animate just fine using this same method of targetting the className of the <circle> element for the map marker. But then, the 'react-way' of fetching data higher up in the component tree, passing it down as props - I get markers but no animation? Puzzling and also frustrating!

 

To whet your whistle, if you're curious I did reach out to the map library creators about a month ago concerning this issue, before I found the bandAid of adding the className as a target, which seemed like a good fix: https://github.com/zcreativelabs/react-simple-maps/issues/119  They did respond with this codesandbox: https://codesandbox.io/s/rzr73rjjq

 

At the time my focus was a bit more on using something like React Transition group, and trying to wrap a new 3rd party component around the 3rd party Marker component was breaking its parent, the map projection itself. The codesandbox does exactly that with a different animation library, but the data is still hard-coded so I might still run into the same issue...I guess I'll find a way to hammer it into place? If you think of any other ideas let me know! I'll of course update the thread with any breakthroughs, and hopefully a better working example so you can see what's going on!

Link to post
Share on other sites

Hi,

 

Well since your markers are in fact SVG circle elements, those are actually the DOM nodes themselves, so you can animate those directly.

 

In the class constructor add this:

 

constructor() {
  // your orther code in the constructor here
  this.markers = [];
}

 

In the render function you should do this:

 

<Markers>
  {data.map((marker, index) => (
    <Marker marker={marker}> 
    <circle className="animateMapMarker" cx={0} cy={0} r={8} fill="#FF5722" stroke="#FFF"
      ref={e => this.markers[index] = e}
    />
    <text y={25} style={{ fontFamily: "Roboto"}} textAnchor="middle">
      {marker.place}
    </text>
      
    </Marker>
  ))}
</Markers>

 

And finally in the component did mount method:

 

this.tl = new TimelineMax({
  //paused: true,
  onUpdate: this.updateSlider
})
.staggerFrom(this.markers, 5, 
  {autoAlpha: 1, ease: Elastic.easOut, scale: 5, yoyo: true, repeat:1}, next);

 

Also a couple of things to keep in mind.

 

You mention that with hard coded data everything works but with the actual server response there are no animations. It sounds very reasonable because passing hardcoded data as props, makes is immediately available. But when the data comes as a response from the server the component has already mounted and the props update only triggers a re-render of the component. The timeline resides in the component did mount event, that triggers only once in the component's life cycle. Perhaps you should use component did update, compare the list of markers and if they are not equal, pause and then kill the timeline, and finally create it again with the updated array. Just be aware of not passing empty arrays because you'll waste time and resources. GSAP is not going to throw an error, because is not going to care about an empty array, but is a waste of CPU cycles.

 

Finally, for drawing the line between the dots you could use the Draw SVG Plugin for that. To center the globe in the dot when each animation starts, perhaps instead of using the stagger method, you could use a call() instance followed by a from() instance for each dot. That would require to loop through the markers array in the did mount or did update method.

 

Happy Tweening!!

  • Like 3
Link to post
Share on other sites

This was a tricky one for me that took a while to figure out...but Rodrigo was spot on in terms of what the problem was...when fetching data asynchronously, you need to make sure the fetch is complete before attempting to render other components. I was getting an error about 'Can’t call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.'

 

In case anyone else is dealing with the same issue - this article here was a huge help! Although this is a React issue, and not GSAP - worth noting how to handle these kinds of things! https://www.robinwieruch.de/react-warning-cant-call-setstate-on-an-unmounted-component/

 

 

  • Like 2
Link to post
Share on other sites

I have a follow-up question, and I've been trolling through the forums and stack overflow in search of a solution that still escapes me at the moment!

 

So...quick recap...I am animating markers on a map of the US, and since there's over 500 an hour, roughly 8,000 a day the stagger methods and TimelineMax have made a very nice visualization - thanks GSAP!

 

-- How to Toggle 'visibility' of tweens throughout the timeline --

The question is that, to avoid showing all of these map markers on render, I've set the CSS for the svg circle element to " visibility: hidden", which I then toggle in the animation using autoAlpha: 1. If I drag the slider to the left to go back in time, or click the reverse or restart buttons, and also when the animation repeats...all of the map markers have now been set to autoAlpha: 1, and every single one that has rendered so far is visible.  Without a better codepen (Still working on the Globe...) and, it looks likes GSAP has many built-in methods to do this, but I haven't found the right one?

 

I've tried chaining to my buttons something like this.tl.restart().pause(0)... as well as clear(), kill() - there's also remove(), Invalidate() - so many options! I must not be using any of them correctly! I've also attempted to use many of these in the onCompleteAll callback that fires at the end of the timeline, still no success! Any ideas?

 

Additionally - and this relates more to react/javascript/dealing with async data fetching...but when the new data set comes in I'm having trouble making a smooth transition from one set of map markers to the next. I know...tough to guess without seeing what's going on, but if you have any tips on best practices or can point me towards examples of someone else doing this that would be huge help!

 

I did do as Rodrigo suggests above and do a deep comparison of the two data sets using the loDash _.isEqual method, inside componentDidUpdate. I was hoping the 'this.forceUpdate() wouldn't be necessary but so far seems to be the only way to tell the D3 map library (react-simple-maps) that it's time to generate new markers? I'm also working on migrating this project into true D3, should be ready for iteration v2!

 

Here's a snippet of that function in my child or map component where it is getting the data as state passed down as props:

	componentDidMount() {
		//Animations with GSAP
		this.animateBookings();
	}
	componentDidUpdate(previousProps) {
		//console.log("DidUpdate child was called!");
		if (!_.isEqual(previousProps.data, this.props.data)) {
			//console.log("DidUpdate child was called no. 2!");
			this.setState({ data: this.props.data });
			this.theWhale();
			this.tl.kill();
			this.animateBookings();
			this.tl.play();
		}
	}

Thanks to everyone who has helped with this project! Big shout out to @Rodrigo !!!

Link to post
Share on other sites
2 hours ago, UnioninDesign said:

The question is that, to avoid showing all of these map markers on render, I've set the CSS for the svg circle element to " visibility: hidden", which I then toggle in the animation using autoAlpha: 1. If I drag the slider to the left to go back in time, or click the reverse or restart buttons, and also when the animation repeats...all of the map markers have now been set to autoAlpha: 1, and every single one that has rendered so far is visible.  Without a better codepen (Still working on the Globe...) and, it looks likes GSAP has many built-in methods to do this, but I haven't found the right one?

Let me see if I can follow this through. You hide your SVG elements using CSS, then using autoAlpha you set them to be visible. Finally you want them to be hidden if you reverse the timeline or restart it. Is that right? Normally restarting the timeline should be enough, since GSAP records the starting values of the instance's targets. This over-simplified example illustrates that. The restart button restarts the timeline and the reset progress button set's the progress to zero and then plays the timeline:

 

See the Pen aMvzpx by rhernando (@rhernando) on CodePen

 

As for the transition between two sets of markers, yeah that's not the easiest task. Before setting the new state there are a couple of considerations. FIrst and most important, is the new set completely different from the previous or it could contain elements from the previous set? If the sets are completely different, then animate out the previous set, when that animation is complete update the state and animate in the new set. If the sets data intersects, then you'll have to filter the existing set, establish the elements that are not present in the new set. Then establish the elements in the new set that are not in the previous set. Animate out the elements in the previous set that are not in the new set, animate in the elements in the new set that are not present in the previous set. Of course in this whole process you need to update the state at the start and figure a way for the elements to be hidden when mounted. Another solution is to use React Transition Group to animate the mounting/unmounting of an element. Here you can see a sample of that:

 

https://stackblitz.com/edit/gsap-react-simple-transition-group?file=simple-transition.js

 

Hopefully this helps in some way.

 

Happy Tweening!!!

  • Like 1
Link to post
Share on other sites

I wanted to reply to myself here (I was formerly @UnioninDesign until I convinced my company to get me a Club Greensock membership) that I never really solved the issue, but I did find a working piece of code...

 

componentWillUnmount() {
         //kills the timeline when data set updates
        this.tl.pause(this.tl.duration()).kill();
    }

So...if you are going back to fetch new data...and in this example, you would be doing that higher up in your component tree, and passing props to this component...Upon receiving new data, React will pass the new props down the component tree, causing the component to unmount before it re-renders with the new props. Those lines of code up there were a big help! But...they do not give you control over how to smoothly transition from one data set to the next! This will kill the timeline as soon as the new data is available, and start a new one with the new data set. Works - yes! Elegant? Maybe kinda sorta?

 

PS I've continued a similar thread here if anyone is interested or curious!

 

  • Like 2
Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×