├── examples ├── src │ ├── components │ │ ├── index.js │ │ ├── Code │ │ │ ├── index.js │ │ │ ├── Code.scss │ │ │ └── Code.jsx │ │ ├── Example │ │ │ ├── index.js │ │ │ ├── Button │ │ │ │ ├── index.js │ │ │ │ ├── Button.jsx │ │ │ │ └── Button.scss │ │ │ ├── Example.scss │ │ │ └── Example.jsx │ │ ├── Examples │ │ │ ├── index.js │ │ │ ├── SpecifyClassName │ │ │ │ ├── SpecifyClassName.scss │ │ │ │ └── index.jsx │ │ │ ├── animate.scss │ │ │ ├── Examples.scss │ │ │ ├── PropChange │ │ │ │ └── index.jsx │ │ │ ├── AtMount │ │ │ │ └── index.jsx │ │ │ ├── AtMountTimeout │ │ │ │ └── index.jsx │ │ │ └── Examples.jsx │ │ ├── App.scss │ │ └── App.jsx │ ├── favicon.gif │ ├── styles │ │ └── _colors.scss │ ├── index.jsx │ └── index.html ├── .babelrc ├── firebase.json ├── publish.js ├── .eslintrc.json ├── .gitignore ├── .sass-lint.yml ├── LICENSE ├── webpack.config.js └── package.json ├── .babelrc ├── misc ├── logo.xcf └── testSetup.js ├── .eslintrc.json ├── .gitignore ├── webpack.config.js ├── circle.yml ├── LICENSE ├── package.json ├── src ├── index.jsx └── index.spec.js ├── README.md └── index.js /examples/src/components/index.js: -------------------------------------------------------------------------------- 1 | export default from './App' 2 | -------------------------------------------------------------------------------- /examples/src/components/Code/index.js: -------------------------------------------------------------------------------- 1 | export default from './Code' 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2017", "es2015", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/src/components/Example/index.js: -------------------------------------------------------------------------------- 1 | export default from './Example' 2 | -------------------------------------------------------------------------------- /examples/src/components/Examples/index.js: -------------------------------------------------------------------------------- 1 | export default from './Examples' 2 | -------------------------------------------------------------------------------- /examples/src/components/Example/Button/index.js: -------------------------------------------------------------------------------- 1 | export default from './Button' 2 | -------------------------------------------------------------------------------- /misc/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unirakun/hoc-react-animate/HEAD/misc/logo.xcf -------------------------------------------------------------------------------- /examples/src/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unirakun/hoc-react-animate/HEAD/examples/src/favicon.gif -------------------------------------------------------------------------------- /examples/src/components/Examples/SpecifyClassName/SpecifyClassName.scss: -------------------------------------------------------------------------------- 1 | :global(.custom) { 2 | transform: scale(.5); 3 | } 4 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-class-properties", "transform-object-rest-spread"], 3 | "presets": ["es2017", "es2015", "react", "stage-0"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/src/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | $primary: #e8f5e9; 2 | $secondary: #a5d6a7; 3 | $color: #fff; 4 | $shadow-start: rgba(0, 0, 0, .16); 5 | $shadow-end: rgba(0, 0, 0, .12); 6 | -------------------------------------------------------------------------------- /examples/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/src/components/Examples/animate.scss: -------------------------------------------------------------------------------- 1 | .component { 2 | transition: all 1000ms; 3 | 4 | &:global(.animate) { 5 | transform: translateX(200px); 6 | } 7 | 8 | pre { 9 | display: inline; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/src/components/Examples/Examples.scss: -------------------------------------------------------------------------------- 1 | .examples { 2 | .example { 3 | margin: 0 auto; 4 | margin-bottom: 20px; 5 | min-width: 900px; 6 | width: 40%; 7 | } 8 | 9 | p { 10 | margin-top: 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/src/components/Code/Code.scss: -------------------------------------------------------------------------------- 1 | @import '~styles/colors'; 2 | 3 | .code { 4 | align-items: center; 5 | background-color: darken($primary, 10); 6 | display: flex; 7 | font-size: 80%; 8 | padding: 20px 10px; 9 | position: relative; 10 | } 11 | -------------------------------------------------------------------------------- /examples/publish.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | var path = require('path'); 3 | 4 | ghpages.publish(path.join(__dirname, 'public'), function(err) { 5 | if(err) { 6 | console.error(err) 7 | } else { 8 | console.log("Success") 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb"], 4 | "rules": { 5 | "semi": [2, "never"], 6 | "arrow-body-style": 0, 7 | "import/no-named-as-default": 0, 8 | "import/prefer-default-export": 0, 9 | "import/no-unresolved": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb"], 4 | "globals": { 5 | "fetch": false 6 | }, 7 | "rules": { 8 | "semi": [2, "never"], 9 | "arrow-body-style": 0, 10 | "import/no-named-as-default": 0, 11 | "import/prefer-default-export": 0, 12 | "import/no-unresolved": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /misc/testSetup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | import chai from 'chai' 3 | 4 | global.document = jsdom('') 5 | global.window = document.defaultView 6 | global.navigator = global.window.navigator 7 | 8 | /** Configuring chai. */ 9 | chai.should() 10 | chai.config.includeStack = true 11 | chai.config.truncateThreshold = 0 12 | -------------------------------------------------------------------------------- /examples/src/components/Example/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import Ink from 'react-ink' 3 | 4 | import styles from './Button.scss' 5 | 6 | const Button = ({ style, className, onClick }) => ( 7 | 11 | ) 12 | 13 | export default Button 14 | -------------------------------------------------------------------------------- /examples/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import 'normalize.css/normalize.css' 5 | 6 | /* eslint import/no-extraneous-dependencies: 0 import/no-unresolved: 0 */ 7 | import 'file?name=[name].[ext]!./index.html' 8 | import 'file?name=[name].[ext]!./favicon.gif' 9 | 10 | import App from './components' 11 | 12 | /* eslint-env browser */ 13 | render( 14 | 15 | , document.getElementById('app') 16 | ) 17 | -------------------------------------------------------------------------------- /examples/src/components/Example/Button/Button.scss: -------------------------------------------------------------------------------- 1 | @import '~styles/colors'; 2 | 3 | .button { 4 | background-color: darken($primary, 20); 5 | border: 0; 6 | box-shadow: none; 7 | color: lighten($secondary, 20); 8 | cursor: pointer; 9 | font-family: 'Roboto Condensed', sans-serif; 10 | font-size: 1.2em; 11 | height: 50px; 12 | outline: 0; 13 | padding: 0 2rem; 14 | position: relative; 15 | text-transform: uppercase; 16 | 17 | &:hover { 18 | background-color: darken($primary, 22); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/src/components/Code/Code.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '!!style!css!highlight.js/styles/github.css' 3 | 4 | import styles from './Code.scss' 5 | 6 | import Highlight from 'react-highlight' 7 | 8 | const Code = ({ style, className, children }) => ( 9 | 10 | {children} 11 | 12 | ) 13 | 14 | Code.propTypes = { 15 | style: PropTypes.object, 16 | className: PropTypes.string, 17 | children: PropTypes.node.isRequired, 18 | } 19 | 20 | export default Code 21 | -------------------------------------------------------------------------------- /examples/src/components/App.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | align-items: center; 3 | display: flex; 4 | flex-direction: column; 5 | font-family: 'Roboto', sans-serif; 6 | 7 | .icon { 8 | left: 10px; 9 | position: absolute; 10 | top: 10px; 11 | width: 100px; 12 | } 13 | 14 | .description { 15 | margin-bottom: 30px; 16 | width: 50%; 17 | } 18 | 19 | .github { 20 | position: absolute; 21 | right: 10px; 22 | top: 10px; 23 | } 24 | 25 | h1 { 26 | font-family: 'Roboto Condensed', sans-serif; 27 | } 28 | } 29 | 30 | :global(.github-button) { 31 | display: none; 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/components/Example/Example.scss: -------------------------------------------------------------------------------- 1 | @import '~styles/colors'; 2 | 3 | .sample { 4 | background-color: $primary; 5 | box-shadow: 0 2px 5px 0 $shadow-start, 0 2px 10px 0 $shadow-end; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | 10 | &:hover { 11 | box-shadow: 0 4px 10px 0 $shadow-start, 0 4px 20px 0 $shadow-end; 12 | } 13 | 14 | .render { 15 | display: flex; 16 | } 17 | 18 | .result { 19 | margin: 10px; 20 | min-width: 700px; 21 | } 22 | 23 | a { 24 | flex: 1; 25 | position: relative; 26 | text-decoration: none; 27 | } 28 | 29 | pre { 30 | margin: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/components/Examples/PropChange/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import animate from 'hoc-react-animate' 3 | 4 | import styles from '../animate.scss' 5 | 6 | const AtMount = ({ ex, className }) => ( 7 |
8 |

9 | {"I'm animated when my "}

ex
{" prop is changed."} 10 |

11 |
ex = {ex}
12 |
13 | ) 14 | 15 | AtMount.propTypes = { 16 | style: PropTypes.object, 17 | className: PropTypes.string, 18 | ex: PropTypes.string.isRequired, 19 | } 20 | 21 | export default animate(AtMount, { watchedProps: ['ex'] }) 22 | -------------------------------------------------------------------------------- /examples/src/components/Examples/AtMount/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import animate from 'hoc-react-animate' 3 | 4 | import styles from '../animate.scss' 5 | 6 | const AtMount = ({ ex, className }) => ( 7 |
8 |

9 | {"I'm animated at mount."} 10 |
11 | {"With default timeout."} 12 |

13 |
ex = {ex}
14 |
15 | ) 16 | 17 | AtMount.propTypes = { 18 | style: PropTypes.object, 19 | className: PropTypes.string, 20 | ex: PropTypes.string.isRequired, 21 | } 22 | 23 | export default animate(AtMount, { atMount: true }) 24 | -------------------------------------------------------------------------------- /examples/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | react-animate: examples 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/src/components/Examples/AtMountTimeout/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import animate from 'hoc-react-animate' 3 | 4 | import styles from '../animate.scss' 5 | 6 | const AtMount = ({ ex, className }) => ( 7 |
8 |

9 | {"I'm animated at mount."} 10 |
11 | {"With custom timeout."} 12 |
13 |

14 |
ex = {ex}
15 |
16 | ) 17 | 18 | AtMount.propTypes = { 19 | style: PropTypes.object, 20 | className: PropTypes.string, 21 | ex: PropTypes.string.isRequired, 22 | } 23 | 24 | export default animate(AtMount, { atMount: true, timeout: 10 }) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | public 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = { 4 | output: { 5 | library: 'ReactLoader', 6 | libraryTarget: 'umd', 7 | }, 8 | 9 | externals: [ 10 | { 11 | react: { 12 | root: 'React', 13 | commonjs2: 'react', 14 | commonjs: 'react', 15 | amd: 'react', 16 | }, 17 | }, 18 | ], 19 | 20 | module: { 21 | loaders: [ 22 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, 23 | ], 24 | }, 25 | 26 | node: { 27 | Buffer: false, 28 | }, 29 | 30 | plugins: [ 31 | new webpack.optimize.OccurenceOrderPlugin(), 32 | new webpack.DefinePlugin({ 33 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 34 | }), 35 | ], 36 | } 37 | -------------------------------------------------------------------------------- /examples/src/components/Examples/SpecifyClassName/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import animate from 'hoc-react-animate' 3 | 4 | import styles from '../animate.scss' 5 | import customStyles from './SpecifyClassName.scss' 6 | 7 | const SpecifyClassName = ({ ex, className }) => ( 8 |
9 |

10 | {"I'm animated when my "}

ex
{" prop is changed."} 11 |
12 | {"My CSS animation class name is customized."} 13 |

14 |
ex = {ex}
15 |
16 | ) 17 | 18 | SpecifyClassName.propTypes = { 19 | style: PropTypes.object, 20 | className: PropTypes.string, 21 | ex: PropTypes.string.isRequired, 22 | } 23 | 24 | export default animate( 25 | SpecifyClassName, 26 | { watchedProps: ['ex'], className: 'custom' } 27 | ) 28 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | 5 | dependencies: 6 | cache_directories: 7 | - examples/node_modules 8 | override: 9 | - docker run --rm -v $(pwd):/usr/src/app zenika/alpine-node npm install 10 | - docker run --rm -v $(pwd)/examples:/usr/src/app zenika/alpine-node npm install 11 | 12 | test: 13 | override: 14 | - docker run --rm -v $(pwd):/usr/src/app zenika/alpine-node npm run test 15 | - docker run --rm -v $(pwd):/usr/src/app zenika/alpine-node npm run lint 16 | - docker run --rm -v $(pwd)/examples:/usr/src/app zenika/alpine-node npm run lint 17 | 18 | deployment: 19 | production: 20 | branch: master 21 | commands: 22 | - npm install -g firebase-tools 23 | - docker run --rm -v $(pwd)/examples:/usr/src/app zenika/alpine-node npm run build 24 | - cd examples && firebase deploy --token=${FIREBASE_TOKEN} --non-interactive --project ${FIREBASE_PROJECT} 25 | -------------------------------------------------------------------------------- /examples/.sass-lint.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | extends-before-mixins: 2 3 | extends-before-declarations: 2 4 | placeholder-in-extend: 0 5 | hex-notation: 2 6 | indentation: 2 7 | property-sort-order: 2 8 | variable-for-property: 2 9 | no-color-literals: 2 10 | clean-import-paths: 2 11 | hex-length: 2 12 | force-pseudo-nesting: 2 13 | force-element-nesting: 2 14 | quotes: 2 15 | class-name-format: 0 16 | empty-line-between-blocks: 2 17 | leading-zero: 2 18 | space-before-brace: 2 19 | space-before-bang: 2 20 | no-important: 0 21 | no-css-comments: 2 22 | no-color-keywords: 2 23 | space-after-colon: 2 24 | no-vendor-prefixes: 0 25 | border-zero: 2 26 | no-empty-rulesets: 2 27 | trailing-semicolon: 2 28 | no-duplicate-properties: 2 29 | shorthand-values: 2 30 | pseudo-element: 2 31 | single-line-per-selector: 2 32 | zero-unit: 2 33 | no-mergeable-selectors: 2 34 | no-qualifying-elements: 2 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 ZenNante5 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Fabien JUIF 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Examples from './Examples' 4 | import styles from './App.scss' 5 | import Icon from 'favicon.gif' 6 | 7 | const App = () => ( 8 |
9 | 10 | 11 |

12 | hoc-react-animate 13 |

14 | 15 |
16 | 25 | Star it on github 26 | 27 |
28 | 29 |

30 | hoc-react-animate is an higher order component to animate React component. It lets you write animated components 31 | as pure functions. 32 |

33 | 34 |

35 | Check out the examples below. Use the button to trigger animations or click the code to read the full source. 36 |

37 | 38 | 39 |
40 | ) 41 | 42 | export default App 43 | -------------------------------------------------------------------------------- /examples/src/components/Example/Example.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import Ink from 'react-ink' 3 | 4 | import Code from 'Code' 5 | import Button from './Button' 6 | import styles from './Example.scss' 7 | 8 | const words = ['Banana', 'House', 'Train', 'Dog', 'Cat', 'River'] 9 | const BASE_URL = 'https://github.com/Zenika/react-animate/blob/master/examples/src/components/Examples/' 10 | 11 | class Example extends Component { 12 | constructor() { 13 | super() 14 | 15 | this.state = { 16 | ex: 'Hi', 17 | } 18 | } 19 | 20 | onClick = () => { 21 | const index = Math.round((Math.random() * 10) % (words.length - 1)) 22 | 23 | this.setState({ 24 | ex: words[index], 25 | }) 26 | } 27 | 28 | render() { 29 | const { style, className, code, children, link } = this.props 30 | 31 | return ( 32 |
33 | {React.cloneElement(children, { ex: this.state.ex, className: styles.result })} 34 | 35 | {code} 36 | 37 | 38 |
40 | ) 41 | } 42 | } 43 | 44 | Example.propTypes = { 45 | style: PropTypes.object, 46 | className: PropTypes.string, 47 | code: PropTypes.string.isRequired, 48 | children: PropTypes.node.isRequired, 49 | link: PropTypes.string.isRequired, 50 | } 51 | 52 | export default Example 53 | -------------------------------------------------------------------------------- /examples/src/components/Examples/Examples.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | import Example from 'Example' 4 | import AtMount from './AtMount' 5 | import AtMountTimeout from './AtMountTimeout' 6 | import PropChange from './PropChange' 7 | import SpecifyClassName from './SpecifyClassName' 8 | 9 | import styles from './Examples.scss' 10 | 11 | const Examples = ({ style, className }) => ( 12 |
13 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 45 | 46 | 47 |
48 | ) 49 | 50 | Examples.propTypes = { 51 | style: PropTypes.object, 52 | className: PropTypes.string, 53 | } 54 | 55 | export default Examples 56 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const dev = (process.env.NODE_ENV !== 'production') 5 | 6 | function getEntrySources(sources) { 7 | if (dev) { 8 | sources.push('webpack-dev-server/client?http://localhost:3000') 9 | sources.push('webpack/hot/only-dev-server') 10 | } 11 | 12 | return sources 13 | } 14 | 15 | function getLoaders(loaders) { 16 | if (dev) { 17 | loaders.push('react-hot') 18 | } 19 | loaders.push('babel') 20 | 21 | return loaders 22 | } 23 | 24 | function getPlugins(plugins) { 25 | if (dev) { 26 | plugins.push(new webpack.HotModuleReplacementPlugin()) 27 | } 28 | 29 | return plugins 30 | } 31 | 32 | module.exports = { 33 | devtool: dev ? 'eval' : '', 34 | entry: { 35 | examples: getEntrySources([ 36 | './src', 37 | ]), 38 | }, 39 | output: { 40 | path: path.join(__dirname, 'public'), 41 | filename: '[name].js', 42 | publicPath: './', 43 | }, 44 | resolve: { 45 | root: [path.resolve('./src'), path.resolve('./src/components')], 46 | extensions: ['', '.js', '.jsx'], 47 | }, 48 | plugins: getPlugins([]), 49 | module: { 50 | loaders: [{ 51 | test: /\.jsx?$/, 52 | loaders: getLoaders([]), 53 | include: [path.join(__dirname, 'src'), path.join(__dirname, '..', 'src')], 54 | }, { 55 | test: /\.s?css$/, 56 | loaders: [ 57 | 'style', 58 | 'css?modules&localIdentName=[path]_[local]__[hash:base64:5]', 59 | 'sass', 60 | ], 61 | }, { 62 | test: /\.(png|svg|gif|jpg)$/, 63 | loader: 'file?name=[name].[ext]', 64 | },], 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hoc-react-animate", 3 | "version": "0.3.0", 4 | "description": "Add a CSS class whenever a props change (or/and at mount)", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "peerDependencies": { 8 | "lodash": "^4.15.0", 9 | "react": "^15.3.0" 10 | }, 11 | "devDependencies": { 12 | "babel-cli": "^6.11.4", 13 | "babel-core": "^6.13.2", 14 | "babel-eslint": "^6.1.2", 15 | "babel-loader": "^6.2.4", 16 | "babel-preset-es2015": "^6.13.2", 17 | "babel-preset-es2017": "^1.6.1", 18 | "babel-preset-react": "^6.11.1", 19 | "babel-preset-stage-0": "^6.5.0", 20 | "chai": "^3.5.0", 21 | "cross-env": "^2.0.0", 22 | "enzyme": "^2.4.1", 23 | "eslint": "^3.2.2", 24 | "eslint-config-airbnb": "^10.0.0", 25 | "eslint-plugin-import": "^1.12.0", 26 | "eslint-plugin-jsx-a11y": "^2.0.1", 27 | "eslint-plugin-react": "^6.0.0", 28 | "jsdom": "^9.4.2", 29 | "lodash": "^4.15.0", 30 | "mocha": "^3.0.2", 31 | "react": "^15.3.0", 32 | "react-addons-test-utils": "^15.3.0", 33 | "react-dom": "^15.3.0", 34 | "webpack": "^1.13.1" 35 | }, 36 | "scripts": { 37 | "lint": "eslint src/index.jsx", 38 | "build": "cross-env BABEL_ENV=cjs babel --ignore \"*.spec.js\" ./src/ -d .", 39 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./misc/testSetup.js \"src/**/*.spec.js\" " 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/Zenika/react-animate.git" 44 | }, 45 | "keywords": [ 46 | "react", 47 | "animate", 48 | "hoc" 49 | ], 50 | "author": "Fabien JUIF ", 51 | "contributors": [ 52 | "Laurent MAILLET " 53 | ], 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/Zenika/react-animate/issues" 57 | }, 58 | "homepage": "https://github.com/Zenika/react-animate#readme" 59 | } 60 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hoc-react-animate-examples", 3 | "version": "0.1.0", 4 | "description": "", 5 | "directories": { 6 | "doc": "doc" 7 | }, 8 | "scripts": { 9 | "start": "set NODE_ENV=development && webpack-dev-server --inline --hot --port=3000 --history-api-fallback", 10 | "build": "NODE_ENV=production webpack --define process.env.NODE_ENV='\"production\"' -p ", 11 | "lint": "eslint src && find ./src/ -iname \"*.scss\" -exec sass-lint -v -q {} +", 12 | "publish_gh_pages": "node publish" 13 | }, 14 | "author": "Fabien JUIF", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "babel-core": "~6.7.5", 18 | "babel-eslint": "~6.0.0", 19 | "babel-loader": "~6.2.4", 20 | "babel-plugin-syntax-class-properties": "~6.5.0", 21 | "babel-plugin-transform-class-properties": "~6.6.0", 22 | "babel-plugin-transform-object-rest-spread": "~6.6.5", 23 | "babel-polyfill": "~6.7.4", 24 | "babel-preset-es2015": "^6.9.0", 25 | "babel-preset-es2017": "^1.4.0", 26 | "babel-preset-react": "~6.5.0", 27 | "babel-preset-stage-0": "~6.5.0", 28 | "babel-register": "~6.7.2", 29 | "chai": "~3.5.0", 30 | "css-loader": "^0.23.1", 31 | "eslint": "^3.2.2", 32 | "eslint-config-airbnb": "^10.0.0", 33 | "eslint-plugin-import": "^1.12.0", 34 | "eslint-plugin-jsx-a11y": "^2.0.1", 35 | "eslint-plugin-react": "^6.0.0", 36 | "expect": "~1.16.0", 37 | "file-loader": "~0.8.5", 38 | "gh-pages": "^0.11.0", 39 | "jsdom": "^9.4.2", 40 | "mocha": "~2.4.5", 41 | "node-sass": "^3.7.0", 42 | "react-hot-loader": "~1.3.0", 43 | "sass-lint": "^1.8.2", 44 | "sass-loader": "^3.2.0", 45 | "sinon": "~1.17.3", 46 | "style-loader": "~0.13.1", 47 | "webpack": "~1.12.2", 48 | "webpack-dev-server": "~1.12.1" 49 | }, 50 | "dependencies": { 51 | "hoc-react-animate": "^0.2.24", 52 | "lodash": "^4.15.0", 53 | "normalize.css": "~4.2.0", 54 | "react": "15.3.0", 55 | "react-dom": "15.3.0", 56 | "react-highlight": "^0.8.0", 57 | "react-ink": "^5.1.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import pick from 'lodash/pick' 3 | import isEqual from 'lodash/isEqual' 4 | 5 | const getDisplayName = (c) => c.displayName || c.name || 'Component' 6 | 7 | export default ( 8 | ComposedComponent, 9 | config, 10 | ) => { 11 | const { 12 | watchedProps = [], 13 | timeout = 1000, 14 | className = 'animate', 15 | atMount = false, 16 | } = config || {} 17 | 18 | return class extends Component { 19 | static displayName = `Animate(${getDisplayName(ComposedComponent)})` 20 | static propTypes = { 21 | className: PropTypes.string, 22 | } 23 | 24 | state = { 25 | props: {}, 26 | } 27 | 28 | applyClassName = (shouldAnimate, props) => { 29 | if (shouldAnimate) { 30 | let composedClassName = className 31 | if (this.props.className) composedClassName = `${composedClassName} ${this.props.className}` 32 | 33 | this.setState({ 34 | props, 35 | className: composedClassName, 36 | }) 37 | 38 | // -1 timeout means we don't delete the animate classname 39 | if (timeout !== -1) { 40 | this.timer = setTimeout( 41 | () => this.applyClassName(false, pick(this.props, watchedProps)), 42 | timeout 43 | ) 44 | } 45 | } else { 46 | this.setState({ 47 | props, 48 | className: this.props.className, 49 | }) 50 | } 51 | } 52 | 53 | componentWillMount() { 54 | this.applyClassName(atMount, pick(this.props, watchedProps)) 55 | } 56 | 57 | componentWillReceiveProps(nextProps) { 58 | const pickedProps = pick(nextProps, watchedProps) 59 | this.applyClassName(!isEqual(pickedProps, this.state.props), pickedProps) 60 | } 61 | 62 | componentWillUnmount() { 63 | clearTimeout(this.timer) 64 | } 65 | 66 | render() { 67 | const newProps = {} 68 | if (this.state.className) newProps.className = this.state.className 69 | 70 | return 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hoc-react-animate 2 | 3 | ## what is this? 4 | This is a higher order component ("HOC") that adds a CSS class to its child component whenever a prop change or at mount (or both) to animate it. 5 | 6 | ## try it 7 | You can test some examples [here](https://react-animate.firebaseapp.com/). 8 | 9 | ## install 10 | `npm i --save hoc-react-animate` 11 | 12 | ## use 13 | You have to wrap your component, and give some informations: 14 | 15 | Parameter | Required | Default value | Description 16 | ----------|--------|---------------|------------- 17 | `watchedProps` | no | `[]` | The props to watch (they are compared with `lodash.isEqual`) 18 | `timeout` | no | `1000` | The time (in ms) for which the CSS class is applied to the wrapped component 19 | `className` | no | `'animate'` | The class to add when a prop changes (or at mount) 20 | `atMount` | no | `false` | Set to `true` if you want to animate the component at mount 21 | 22 | **Component.js** 23 | ```(javascript) 24 | import React, { PropTypes } from 'react' 25 | import animate from 'hoc-react-animate' 26 | 27 | const Component = ({ className, text }) => { 28 | return ( 29 |
32 | {text} 33 |
34 | ) 35 | } 36 | 37 | Component.propTypes = { 38 | className: PropTypes.string, 39 | text: PropTypes.string, 40 | } 41 | 42 | export default animate( 43 | Component, 44 | { 45 | watchedProps: ['text'], 46 | timeout: 200, 47 | } 48 | ) 49 | ``` 50 | 51 | **css** 52 | ```(css) 53 | .component { 54 | transition: all .2s; 55 | } 56 | .component.animate { 57 | transform: scale(2); 58 | } 59 | ``` 60 | 61 | # About uni rakun 62 | **uni rakun** is created by two passionate french developers. 63 | 64 | Do you want to contact them ? Go to their [website](https://unirakun.fr) 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Guillaume CRESPELFabien JUIF
75 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import animate from './index' 4 | 5 | /* eslint-env mocha */ 6 | 7 | const Component = () =>
8 | const getWrapped = (config, props) => { 9 | const Container = animate(Component, config) 10 | return mount() 11 | } 12 | 13 | const getProps = (mounted) => mounted.find(Component).props() 14 | const equals = (mounted, object) => getProps(mounted).should.be.deep.equals(object) 15 | 16 | describe('react-animate', () => { 17 | it('should have the right default parameters', () => { 18 | const animated = getWrapped() 19 | 20 | equals(animated, {}) 21 | }) 22 | 23 | it('should not filter props, animate at mount for 1s', done => { 24 | const animated = getWrapped({ atMount: true }, { testProps: '1' }) 25 | 26 | setTimeout(() => { 27 | equals(animated, { 28 | className: 'animate', 29 | testProps: '1', 30 | }) 31 | 32 | setTimeout(() => { 33 | equals(animated, { 34 | testProps: '1', 35 | }) 36 | done() 37 | }, 900) 38 | }, 100) 39 | equals(animated, { 40 | className: 'animate', 41 | testProps: '1', 42 | }) 43 | }) 44 | 45 | it('should not delete animate classname', done => { 46 | const animated = getWrapped({ atMount: true, timeout: -1 }, { testProps: '1' }) 47 | 48 | setTimeout(() => { 49 | equals(animated, { 50 | className: 'animate', 51 | testProps: '1', 52 | }) 53 | 54 | setTimeout(() => { 55 | equals(animated, { 56 | className: 'animate', 57 | testProps: '1', 58 | }) 59 | done() 60 | }, 1100) 61 | }, 100) 62 | equals(animated, { 63 | className: 'animate', 64 | testProps: '1', 65 | }) 66 | }) 67 | 68 | it('should animate at mount for 10ms with custom class', done => { 69 | const animated = getWrapped({ className: 'test', atMount: true, timeout: 10 }) 70 | 71 | setTimeout(() => { 72 | equals(animated, { }) 73 | done() 74 | }, 10) 75 | 76 | equals(animated, { className: 'test' }) 77 | }) 78 | 79 | it('should merge classNames', done => { 80 | const animated = getWrapped({ atMount: true, timeout: 10 }, { className: 'test' }) 81 | 82 | setTimeout(() => { 83 | equals(animated, { className: 'test' }) 84 | done() 85 | }, 10) 86 | 87 | equals(animated, { className: 'animate test' }) 88 | }) 89 | 90 | it('should animate on props changing', done => { 91 | const animated = getWrapped({ timeout: 10, watchedProps: ['test'] }, { test: 'oldValue' }) 92 | 93 | equals(animated, { 94 | test: 'oldValue', 95 | }) 96 | 97 | animated.setProps({ test: 'oldValue', newProp: 'test' }) 98 | equals(animated, { 99 | test: 'oldValue', 100 | newProp: 'test', 101 | }) 102 | 103 | animated.setProps({ test: 'newValue', newProp: 'update' }) 104 | setTimeout(() => { 105 | equals(animated, { 106 | test: 'newValue', 107 | newProp: 'update', 108 | }) 109 | done() 110 | }, 10) 111 | 112 | equals(animated, { 113 | className: 'animate', 114 | test: 'newValue', 115 | newProp: 'update', 116 | }) 117 | }) 118 | 119 | it('should clear timeout', done => { 120 | /* eslint-disable no-underscore-dangle */ 121 | const animated = getWrapped({ atMount: true }) 122 | setTimeout(() => { 123 | animated.node.timer._called.should.be.deep.equals(false) 124 | }, 10) 125 | 126 | animated.unmount() 127 | animated.node.timer._called.should.be.deep.equals(false) 128 | animated.node.timer._idleTimeout.should.be.deep.equals(-1) 129 | 130 | setTimeout(() => { 131 | animated.node.timer._called.should.be.deep.equals(false) 132 | done() 133 | }, 1000) 134 | 135 | /* eslint-enable no-underscore-dangle */ 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _pick = require('lodash/pick'); 16 | 17 | var _pick2 = _interopRequireDefault(_pick); 18 | 19 | var _isEqual = require('lodash/isEqual'); 20 | 21 | var _isEqual2 = _interopRequireDefault(_isEqual); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 26 | 27 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 28 | 29 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 30 | 31 | var getDisplayName = function getDisplayName(c) { 32 | return c.displayName || c.name || 'Component'; 33 | }; 34 | 35 | exports.default = function (ComposedComponent, config) { 36 | var _class, _temp2; 37 | 38 | var _ref = config || {}; 39 | 40 | var _ref$watchedProps = _ref.watchedProps; 41 | var watchedProps = _ref$watchedProps === undefined ? [] : _ref$watchedProps; 42 | var _ref$timeout = _ref.timeout; 43 | var timeout = _ref$timeout === undefined ? 1000 : _ref$timeout; 44 | var _ref$className = _ref.className; 45 | var className = _ref$className === undefined ? 'animate' : _ref$className; 46 | var _ref$atMount = _ref.atMount; 47 | var atMount = _ref$atMount === undefined ? false : _ref$atMount; 48 | 49 | 50 | return _temp2 = _class = function (_Component) { 51 | _inherits(_class, _Component); 52 | 53 | function _class() { 54 | var _ref2; 55 | 56 | var _temp, _this, _ret; 57 | 58 | _classCallCheck(this, _class); 59 | 60 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 61 | args[_key] = arguments[_key]; 62 | } 63 | 64 | return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref2 = _class.__proto__ || Object.getPrototypeOf(_class)).call.apply(_ref2, [this].concat(args))), _this), _this.state = { 65 | props: {} 66 | }, _this.applyClassName = function (shouldAnimate, props) { 67 | if (shouldAnimate) { 68 | var composedClassName = className; 69 | if (_this.props.className) composedClassName = composedClassName + ' ' + _this.props.className; 70 | 71 | _this.setState({ 72 | props: props, 73 | className: composedClassName 74 | }); 75 | 76 | // -1 timeout means we don't delete the animate classname 77 | if (timeout !== -1) { 78 | _this.timer = setTimeout(function () { 79 | return _this.applyClassName(false, (0, _pick2.default)(_this.props, watchedProps)); 80 | }, timeout); 81 | } 82 | } else { 83 | _this.setState({ 84 | props: props, 85 | className: _this.props.className 86 | }); 87 | } 88 | }, _temp), _possibleConstructorReturn(_this, _ret); 89 | } 90 | 91 | _createClass(_class, [{ 92 | key: 'componentWillMount', 93 | value: function componentWillMount() { 94 | this.applyClassName(atMount, (0, _pick2.default)(this.props, watchedProps)); 95 | } 96 | }, { 97 | key: 'componentWillReceiveProps', 98 | value: function componentWillReceiveProps(nextProps) { 99 | var pickedProps = (0, _pick2.default)(nextProps, watchedProps); 100 | this.applyClassName(!(0, _isEqual2.default)(pickedProps, this.state.props), pickedProps); 101 | } 102 | }, { 103 | key: 'componentWillUnmount', 104 | value: function componentWillUnmount() { 105 | clearTimeout(this.timer); 106 | } 107 | }, { 108 | key: 'render', 109 | value: function render() { 110 | var newProps = {}; 111 | if (this.state.className) newProps.className = this.state.className; 112 | 113 | return _react2.default.createElement(ComposedComponent, _extends({}, this.props, newProps)); 114 | } 115 | }]); 116 | 117 | return _class; 118 | }(_react.Component), _class.displayName = 'Animate(' + getDisplayName(ComposedComponent) + ')', _class.propTypes = { 119 | className: _react.PropTypes.string 120 | }, _temp2; 121 | }; --------------------------------------------------------------------------------