Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling React animation with horizontal scrolling

I'm trying to create an horizontally scrolling page navigation with animations using React and React.addons.CSSTransitionGroup. Currently I'm able to do the horizontal scrolling (with flexbox), page opening/closing, animating entering and leaving. But the animation is not exactly what I want.

Take a look at this example (jsfiddle).

When you click on a buttons on a previous page it currently pushes the pages that are leaving the screen to the right. Although the correct result would be to animate the leaving pages in the same place. I'm not sure how to achieve that effect with CSSTransitionGroup or know whether it is possible with it at all.

function generateId() {
  var r = "";
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  for (var i = 0; i < 10; i += 1) {
    r += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return r;
}

var PageList = {
  pages: [],
  listener: function(newpages) {},
  open: function(caption, index) {
    if (index != null) {
      this.pages = this.pages.slice(0, index + 1);
    }
    this.pages.push({
      id: generateId(),
      caption: caption,
      width: (150 + Math.random() * 50) | 0
    });
    this.listener(this.pages);
  }
};
PageList.open("Main");

var PageLink = React.createClass({
  render: function() {
    var self = this;
    return React.DOM.button({
      className: "pagelink",
      onClick: function() {
        PageList.open(self.props.caption, self.props.pageIndex);
      }
    }, self.props.caption);
  }
});

var Page = React.createClass({
  render: function() {
    return React.DOM.article({
        className: "page",
        style: {
          width: this.props.page.width + "px"
        }
      },
      React.DOM.h1({}, this.props.page.caption),
      React.createElement(PageLink, {
        caption: "Alpha",
        pageIndex: this.props.index
      }),
      React.createElement(PageLink, {
        caption: "Beta",
        pageIndex: this.props.index
      }),
      React.createElement(PageLink, {
        caption: "Gamma",
        pageIndex: this.props.index
      })
    );
  }
});

var Pages = React.createClass({
  componentDidMount: function() {
    var self = this;
    PageList.listener = function(pages) {
      self.setState({
        pages: pages
      });
    };
  },
  getInitialState: function() {
    return {
      pages: PageList.pages
    };
  },
  render: function() {
    var pages = this.state.pages.map(function(page, index) {
      return React.createElement(Page, {
        key: page.id,
        index: index,
        page: page
      });
    });
    return React.createElement(
      React.addons.CSSTransitionGroup, {
        component: "section",
        className: "pages",
        transitionName: "fall"
      }, pages);
  }
});

React.initializeTouchEvents(true);
React.render(
  React.createElement(Pages),
  document.getElementById("main")
);
/* animation */

.fall-enter {
  transform: translate(0, -100%);
  transform: translate3d(0, -100%, 0);
}
.fall-enter.fall-enter-active {
  transform: translate(0, 0);
  transform: translate3d(0, 0, 0);
  transition: transform 1s ease-out;
}
.fall-leave {
  index: -1;
  transform: translate(0, 0);
  transform: translate3d(0, 0, 0);
  transition: transform 1s ease-in;
}
.fall-leave.fall-leave-active {
  transform: translate(0, 100%);
  transform: translate3d(0, 100%, 0);
}
/* other */

* {
  box-sizing: border-box;
}
.pagelink {
  padding: 5px;
  margin: 5px 0px;
  width: 100%;
}
.pages {
  display: flex;
  flex-flow: row;
  overflow-x: scroll;
  overflow-y: hidden;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #ddd;
}
.pages:after {
  flex: none;
  -webkit-flex: none;
  display: block;
  content: " ";
  width: 100px;
}
.page {
  flex: none;
  -webkit-flex: none;
  margin: 8px;
  padding: 10px 31px;
  background: #fff;
  overflow: auto;
  box-shadow: 2px 1px 4px rgba(0, 0, 0, 0.2);
  border-radius: 3px;
}
<body id="main">
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.12.2/react-with-addons.js"></script>

PS: It's fine if it only works with the newest browsers.

like image 934
Egon Avatar asked Jan 11 '15 14:01

Egon


People also ask

How do I scroll horizontally in React?

This is a horizontal scrolling menu component for React. Menu component has adaptive width, just set width for parent container. Items width will be determined from CSS styles. For navigation, you can use scrollbar, native touch scroll, mouse wheel or drag by mouse.

How do you handle scrolling in React?

To handle the onScroll event in React: Set the onScroll prop on an element or add an event listener on the window object. Provide an event handler function. Access relevant properties on the event or window objects.

Is React good for animations?

React Transition Group is a good animation library and has a very small bundle size. It's one of the most popular animation libraries and should be considered for your next React project.


1 Answers

Problem

To get the vertical lineup of pages that you're looking for, we're going to have to change the HTML to add an extra layer. This extra layer will be a vertical channel that the articles slide up and down in. Right now, new ones are just being pushed to the side because there is no parent layer to contain them.

Second problem is that all the HTML is created dynamically by React. Trying to go in and change a library/plugin is not a good idea, so I tried to create the effect you're going for with jQuery instead.

Possible Solution

HTML Structure

First thing I did was sketch up a desired layout. We need three layers inside the overall parent:

Parent
    Cell
        Innercell (doubles in height to contain two articles)
            Article

The dotted line in the sketch is the Innercell, the Page rectangles are the Articles

enter image description here

CSS

For the most part, I'm using your original styling. The main difference is that in the extra layers between the cell and article. The cell keeps it's height fixed at 100%, then the innercell gets height:200%. It's also positioned absolutely, with bottom:0. This is important, because it means the extra spacing is above the parent instead of below. We also make sure that the article itself is positioned absolutely, with bottom:0. This places it in the bottom half of the innercell.

JS

First thing to do here is to figure out the order we want things to happen. There are three possible routes we can go when a button is clicked.

  1. If a rightmost button is clicked, we want to add a new cell, then slide in an article.

  2. If a second-to-rightmost button is clicked, we just want to slide in a new article.

  3. If any other button is clicked, we want to slide in a new article in the cell to the right, and all further right cells should slide down with no replacements.

enter image description here

Inside the the change article circle, we need to:

1 Create new article from template, insert all variable data, and place it in the empty space in the top of the innercell.

2 We need to slide the innercell down.

3 After the animation is done, we need to remove the previous article and reset the innercell

Final Code

var cellTemplate = '<cell data-col="DATACOL"><innercell><article></article></innercell></cell>';
var innerTemplate = '<article class="new"><innerarticle><h1>TITLE</h1><button>Alpha</button><button>Beta</button><button>Gamma</button></innerarticle></article>';
var numCells = 2;
var animating = 0;

$('body').on('click', 'article button', function(event){
    if (!animating) {
        var currentCell = parseInt($(this).parent().parent().parent().parent().attr('data-col'));
        var title = $(this).text();
        if (currentCell == numCells) {
            addCell(title);
        } else if (currentCell == numCells - 1) {
            changeArticle(title,numCells);
        } else {
            removeExtraCells(title,currentCell);
        }
    }
});

function removeExtraCells(title,currentCell) {
    var tempCurrentCell = currentCell;
    changeArticle(title,tempCurrentCell+1);
    while (tempCurrentCell < numCells) {
        tempCurrentCell++;
        deleteArticle(title,tempCurrentCell+1);
    }
    numCells = currentCell+1;
}

function addCell(title){
    numCells++
    var html = cellTemplate.replace('DATACOL',numCells);
    $('section').append(html);
    changeArticle(title,numCells);
}

function changeArticle(title,changingCol) {
    var cell = $('cell[data-col="'+changingCol+'"] innercell');
    var html = innerTemplate.replace('TITLE', title).replace('DATACOL', numCells - 1);
    cell.prepend(html);
    triggerAnimation(cell);
}

function deleteArticle(title,changingCol) {
    var cell = $('cell[data-col="'+changingCol+'"] innercell');
    var html = '<article></article>';
    cell.prepend(html).addClass('deleting');
    triggerAnimation(cell);
}

function triggerAnimation(cell) {
    cell.addClass('animating');
    window.setTimeout(function(event){
        cell.css('bottom','-100%');
        animating = 1;
    },50);
}

$('body').on('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', 'innercell', function(event){
    if ( $(this).hasClass('deleting') ) {
        $(this).parent().remove();
    } else {
        $(this).children('article:last-child').remove();
        $(this).children('article.new').removeClass('new');
        $(this).removeClass('animating');
        $(this).css('bottom','0');
    }
    animating = 0;
});
/* other */
 * {
    box-sizing: border-box;
}

section {
    display: flex;
    flex-flow:row;
    overflow-x: scroll;
    overflow-y: hidden;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: #ddd;
}

cell {
    flex: none;
    -webkit-flex: none;
    display:block;
    height:100%;
    float:left;
    position:relative;
    width:166px;
}
innercell {
    display:block;
    height:200%;
    width:100%;
    position:absolute;
    left:0;
    bottom:0;
}
.animating {
    transition: bottom 1s ease-out;
}
article {
    display:block;
    padding:8px;
    height:50%;
    position:absolute;
    left:0;
    bottom:0;
}
article.new {
    bottom:auto;
    top:0;
}
innerarticle {
    display:block;
    padding: 10px 31px;
    background: #fff;
    overflow: auto;
    box-shadow: 2px 1px 4px rgba(0, 0, 0, 0.2);
    border-radius: 3px;
    height:100%;
}

innerarticle button {
    padding: 5px;
    margin: 5px 0px;
    width: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<section>
    <cell data-col="1">
        <innercell>
            <article>
                <innerarticle>
                    <h1>Main</h1>
                    <button>Alpha</button>
                    <button>Beta</button>
                    <button>Gamma</button>
                </innerarticle>
            </article>
        </innercell>
    </cell>
    <cell data-col="2">
        <innercell>
            <article>
                <innerarticle>
                    <h1>Alpha</h1>
                    <button>Alpha</button>
                    <button>Beta</button>
                    <button>Gamma</button>
                </innerarticle>
            </article>
        </innercell>
    </cell>
</section>

Notes

I did end up adding a fourth layer, which I called innerarticle. This was because you had an 8px margin on your article. With two articles on top of each other vertically, the margin collapses and doesn't stack. Be adding an innerarticle and then giving the article a padding:8px, I could make sure it stacks.

Also, you may have noticed that I'm using a lot of custom element names. I do that because it works and it makes it easy for you to understand what's doing on. You can feel free to change them all to <div class="INSERTCURRENTELEMENT">

like image 77
Andy Mercer Avatar answered Sep 28 '22 02:09

Andy Mercer