On Github josephinehall / react-touch-animate
Josephine Hall / @jojoconstance
Reppin' Icelab and the ACT
I'm going to be talking today about some techniques we at Icelab used recently on a React project dealing with css animations, and responding to user touch events.User interfaces, components and state
So what do you need to know about React?
var Button = React.createClass({ render: function() { if (this.state.disabled){ //don't display the button } else { return ( <div onClick={this._getLocation} className="find-button"> <span className="find-button__text">Nearby</span> <i className="find-button__icon"/> </div> ); } }, _getLocation: function() { AppDispatcher.trigger("geolocator:request"); this.setState({ disabled: true }); } }); React.renderComponent( <Button /> document.body );
In this component responding to user input (via _getLoaction) modifies the state of the component, triggering a re-render. React components *know* their data and can figure out their state rather than inferring it from the DOM.
For me, it was helpful to think of components as objects (object oriented programming), rather than a series of maniuplations on dom elements.
A good example is an image carousel - when you create the carousel, you pass in the images that will be rendered as a property. The component then takes that data and figures out how it needs to be rendered. This might include things like calculating the width of wrapper divs based on the number of imaged you passed in.
JSX block:
<div> <TouchPreview previewClassName="link--preview"> <a href="/foo"> Hello </a> </TouchPreview> </div>
CSS:
a { color: blue; } .link--preview a { color: red; text-decoration: underline; }
TouchPreview = React.createClass({ //sets the default preview class name if not passed in by this.props getDefaultProps: function() { return { previewClassName: "preview" }; }, getInitialState: function() { return { preview: false }; }, render: function() { //extend the built in React events to include our behaviour var props = _.extend({ onTouchStart: this._startPreview, onMouseOver: this._startPreview, onTouchEnd: this._endPreview, onTouchMove: this._endPreview, onMouseOut: this._endPreview }, this.props); //set the class property to be our preview class props.className = (this.state.preview) ? this.props.previewClassName : ""; //clear out the props so that the class can be different next time delete props.previewClassName; //return a span with all these properties // set on it, preserving the child elements return React.DOM.span(props, this.props.children); }, _startPreview: function(e) { this.setState({ preview: true }); }, _endPreview: function(e) { this.setState({ preview: false }); } });Since JSX is JavaScript, identifiers such as class and for are discouraged as XML attribute names. Instead, React DOM components expect attributes like className and htmlFor, respectively.
Animating a list that might get re-rendered, say what??
We wanted to make nice smooth transitions on large nested lists that could be re-rendered while they’re animated
Using it...
_renderResultType: function(type) { var results = this._renderResults(this.props.latLng, type); return ( <AnimationItem key={type+"group"} prefix="result-group"> <div className="results-list-body__group"> {results} </div> </AnimationItem> ); }
var AnimationItem = React.createClass({ componentWillEnter: function(done) { this.el = this.getDOMNode(); this.$el = $(this.el); // Before state applied immediately this.$el.addClass(this.props.prefix+"-enter-before"); // Ensure parent class changes propagate in time requestAnimationFrame(function() { this.$el .removeClass(this.props.prefix+"-enter-before") .addClass(this.props.prefix+"-enter"); requestAnimationFrame(function() { getComputedStyle(this.el); this.$el.addClass(this.props.prefix+"-enter-active"); Arrival.complete(this.$el, done); }.bind(this)); }.bind(this)); }, componentDidEnter: function() { this.$el .removeClass(this.props.prefix+"-enter") .removeClass(this.props.prefix+"-enter-active"); }, componentWillLeave: function(done) { this.el = this.getDOMNode(); this.$el = $(this.el); // Before state applied immediately this.$el.addClass(this.props.prefix+"-leave-before"); requestAnimationFrame(function() { this.$el .removeClass(this.props.prefix+"-leave-before") .addClass(this.props.prefix+"-leave"); requestAnimationFrame(function() { this.$el.addClass(this.props.prefix+"-leave-active"); Arrival.complete(this.$el, done); }.bind(this)); }.bind(this)); }, componentDidLeave: function() { this.$el .removeClass(this.props.prefix+"-leave") .removeClass(this.props.prefix+"-leave-active"); }, render: function() { return ( React.DOM.div( {className: "animation-item", key: this.props.key}, this.props.children ) ) } });
ReactTransitionGroup - gives us the extra lifecycle hooks
We have to do multiple requestAnimationFrame calls to make sure that the browser has rendered class attributes further up the tree that we’re relying on. In this app we add a little .direction--up or .direction--down class to the layout block to signify if you’re moving up or down the perceived navigation stack as well.
jQuery! We needed a way to actually manipulate the classes. We only had to use it in two or three places across the whole app - once you get used to how React works you find you don't really need it
If we made this again, we'd probably give javascript specified animations a go.
All due credit for these goes go my esteemed colleague Max (@makenosound), he knows a thing or two