Jump to content
GreenSock

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

Mapping through arrays - list items the react way, with conditional logic

Recommended Posts

Hi team!

 

I've been struggling for a while to get some conditional logic in place with my animations. Some quick backstory to give my codepen some context...Anyone who's curious can also see my two other threads, all related to the same project, but I thought I would make a new one with a greatly reduced example. There are LOTS of other questions about animating list items, but not many with react? And none that I've seen that involve potentially large amounts of data, or conditional logic mixed into the timeline?

 

Backstory: I've built an interactive USA map using D3/react-simple-maps, it shows all of my companies bookings on a 5 minute delay...usually close to 50 at a time...100-150 an hour, about 8,000 in a day! That's a lot dots to show on a map! Thanks GSAP for your awesome timeline and stagger methods to make it more digestible.

 

So...enter the 'ticker'... I decided in addition to the map I would add a sidebar that has a card with data on each booking, like who it is, the dollar amount etc. These cards appear in the ticker at the same time dots appear on the map. Now that we're live, everyone wants new features. "Wouldn't it be cool if there was a booking over a certain dollar amount, we could show a whale fly across the screen?" How do I that????

 

A couple of things, then some code!

- Because these ticker cards contain lots of data, I do want them to pause in place for long enough that someone could read them if they wanted...after digging through forums and lots of codepen examples, the best I could come up with was multiple staggerFromTos chained together! It's pretty choppy but it works...ish. The one that is live has better timing than the the codepen! Also - no map in this example - just list items...and a cheeseburger callback!

- I've been playing around with .add and .call, or adding a label somewhere to my timeline - not quite there yet!

- Nothing I've done actually considers the data! In my codepen, I've created an array with id, and values, and I'm hoping if a card reaches the top of the box (or anywhere in the box) and has a value greater than 10, it will call the cheesburger animation to do its thing? Any ideas? what am I doing wrong? Where should this conditional logic live - within the function, or outside of the timeline somewhere...possible in componentDidMount or DidUpdate?

- I'm still getting the hang of react and its ways...but our best practice is to use keys when going through an array with the .map() method, and use createRefs to set targets for the animation. Is there a way I can access a particular card by it's key or ref etc?

 

https://stackblitz.com/edit/react-iiesig?embed=1&file=index.js

Thanks!

 

See the Pen by edit (@edit) on CodePen

Link to comment
Share on other sites

Hi,

 

Well, there are a few ways to tackle this. The ideal react-way should be to keep track of the current card's index position in the array and check that value in order to update the component's state with the card's value, and if the updated state is bigger than 10, run the animation. Unfortunately your code is not structured like that and honestly I don't see the need to refactor everything if the only side effect of an animation update is trigger another animation and not data or DOM updates.

 

With that in mind the solution is to mess around with scope and this, which until this day and after working in JS for so long it can come up as an obscure and difficult topic. Lucky for us GSAP does helps a lot in terms of binding and managing scope.

 

First in the render method add an extra attribute to the DOM element created in the JSX:

<li className="dataCard" key={id} ref={e => (this.listItem[id] = e)} data-value={listItem.value}>
  <p>ID: <span>{listItem.id}</span></p>
  <p>Value: <span>{listItem.value}</span></p>
</li>

 

As you can see I'm passing the data-value attribute which is the value you want to check.

 

Then in the GSAP part of your code you have to use an onComplete callback inside the configuration of the stagger method, not using the onCompleteAll callback:

 

animateDataCards(){
  this.tl = new TimelineLite({repeat: -1 });
  const duration = 1;
  const next = 2;
  this.tl
  .staggerFromTo(this.listItem, duration, {autoAlpha: 0, y:500},{autoAlpha: 1, y:275}, next)
  .staggerFromTo(this.listItem, duration, {y:275}, {y:125}, next, 2.5)
  .staggerFromTo(this.listItem, duration, {y:125}, { y:10, onComplete: this.burgerFlip, onCompleteParams: ["{self}"] }, next, 2.5)
  .staggerFromTo(this.listItem, 1, {y:10},{ autoAlpha: 0, y: -250, ease: Power3.easeInOut },5, this.onCompleteAll)
}

 

Now the key here are two GSAP specific concepts. The first concept is the fact that any stagger method returns an array of GSAP instances, so each loop in the array passed to the stagger method, generates a specific TweenLite method in this case. Because of that we can attach a callback on each of those. The second, and I'm sure you already saw it, is the "{self}" string passed as the parameter for that specific callback. That tells GSAP to pass the instance as the first parameter in the callback.

 

Then in the burger flip callback you can run this code:

burgerFlip(instance){ 
  const dataValue = instance.target.getAttribute("data-value");
  if ( dataValue > 10 ){
    TweenLite.fromTo(".burger", 3, {x: -250, autoAlpha:0, rotation: 0}, {autoAlpha:1, rotation:720, x:250})
  } else {
    console.log("sorry no burger");
  }
}

 

Here is the kicker, each GSAP instance has a target property, which is a reference of the DOM node being animated, hence instance.target in the code. Since we attached a data-value attribute to the element, we can now check that value and run our conditional logic and see if the burger will animate or not:

 

https://stackblitz.com/edit/react-gsap-burger-conditional

 

Happy Tweening!!

  • Like 4
Link to comment
Share on other sites

As always - thanks @Rodrigo - works great! Adding the data-attribute to my list items totally makes sense - but glad I made this post, I don't think I would have figured out the 'instance.target.getAttribute' part on my own!

  • Like 2
Link to comment
Share on other sites

HI friends - I noticed a bug and wanted to post a quick update in case anyone is working on a similar project. I never got any errors, but noticed that sometimes my animation would fire when it wasn't supposed to? I have two theories as to why this happened, and wanted to share my fix:

  • From my research debugging this issue, it sounds like data-attributes may not be the most stable?
  • As I am going through the data using .map(), and then checking data-attributes for a certain value....but also repeating my timeline until the next data fetch, I believe the function was being called again when it hit the same index or position in the array where it had been called previously, regardless of the value? My console logs did indeed confirm that this was from a previous booking!

So here's what I did, with snippet below:

  • After each time the 'whale' animation, or in this example burgerFlip, use setAttribute to add a new data-attribute
  • in the conditional logic, add a check using the hasAttribute() method to see if the new attribute was present before calling the function.

I haven't updated the codepen yet, but it would look something like this:

burgerFlip(instance){ 
  const dataValue = instance.target.getAttribute("data-value");
  //check if the instance had called the burgerFlip previously...
  const prevBurger = instance.target.hasAttribute("data-burgerflipper"); //returns true or false
  if ( dataValue > 10 && prevBurger == false ){
    TweenLite.fromTo(".burger", 3, {x: -250, autoAlpha:0, rotation: 0}, {autoAlpha:1, rotation:720, x:250})
    //add a data-attribute to the instance that had called the burgerFlip function...
    instance.target.setAttribute("data-burgerflipper", "_self"); //per MDN docs you need to add "_self" to specify the instance in question or you get an error
  } else {
    console.log("sorry no burger");
  }
}

Thanks for the help and support as always!

Link to comment
Share on other sites

Follow up question...how do I capture the instance of the <li> element itself?


What I'm trying to do...since these cards showcase quite a bit of data...is to add additional animations to the card or <li> itself if certain conditions are met. The data-attributes method works great for spotting a certain value etc, but then triggering another animation doesn't necessarily make it clear why a burger, or a whale in my case, is flying across the screen?

 

From my research (and errors!) you can't tween a data-attribute, nor is it possible to render a data attribute (like take the value from the attribute and pass it to a <h2> tag or something?).  I suspect that, because I had already used .map to go through and animate the entire array with staggerFromTo I may be complicating things? Perhaps a for each loop is in order? Any ideas are appreciated!

Link to comment
Share on other sites

In order to get the instance of the <li> element in the DOM you can use refs. If that's not what you're looking for, please be more specific about it.

 

Regarding the second part I'm a bit lost. The first part is that you want to add an extra animation depending on some specific criteria for a specific card. You can use the same array.map() to store some data in an array and then return the array of JSX elements. Keep in mind that the callback inside a .map() method has to return some element that is added to an array, but before that you can run any logic you want. Then you can use a for or for each loop to create your animations and check there if the extra instance has to be created using either the original data or a new array you can create in the .map() method. Is also important to remember that the .map() method goes through an array in ascending order, that is starts with the element at index 0. Because of that, the order in which the DOM elements are added to the array that will hold their references created in React, is the same the data for each element in the original array, so you can easily use a forEach and use the index value in the callback to read the original data and avoid adding more attributes to the DOM element.

 

// original data
const originalData = [];

//in the component constructor
constructor (props) {
  super(props);
  this.animateCards = this.animateCards.bind(this);
  this.cards = [];
}

// in the render method
render () {
  <div>
    {originalData.map (card, index) => (<li ref={e => this.cards[index] = e}>Element</li>)}
  </div>
}

// finally when creating the animations
animateCards () {
  this.cards.forEach((card, index) => {
    // here this.cards[index] matches the data in originalData[index]
    // you can use originalData[index] to deicde if an extra animation is needed
  });
}

 

Finally if you want to add an extra DOM element depending on the data, you should do that in the array.map() inside the render method and not somewhere else. Also base that on the original data and not some attribute passed to the rendered DOM element that results from the .map() method.

 

Please do your best to provide some small and simplified sample code in order to get a better idea of what you're trying to do.

 

Happy Tweening!!

  • Like 2
Link to comment
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.

×