Egon
Egon

Reputation: 1705

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.

Upvotes: 5

Views: 7150

Answers (2)

Baraa Al-Tabbaa
Baraa Al-Tabbaa

Reputation: 753

Came up with an easy solution by adding this to the css:

.fall-enter.fall-enter-active {
    position: absolute;
    height: 97%;
    margin-top: -5px;
}
.fall-enter.fall-enter-active, .fall-leave.fall-leave-active {
    transition: transform 1s ease-in;
}

Working example: http://jsfiddle.net/6985qr33/5/

Have fun

Upvotes: 2

Kelderic
Kelderic

Reputation: 6687

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">

Upvotes: 12

Related Questions