├── .eslintrc.js
├── .gitignore
├── .storybook
├── addons.js
└── config.js
├── README.md
├── gifs
├── array_update.gif
├── basic_enter_exit.gif
└── group_update.gif
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.js
├── components
│ ├── Item
│ │ └── index.js
│ └── List
│ │ └── index.js
├── examples
│ ├── ArrayUpdateExample
│ │ ├── AddArrayAnimationsHOC.js
│ │ ├── animations.js
│ │ └── index.js
│ ├── BasicEnterExitExample
│ │ ├── AddAnimationsHOC.js
│ │ ├── animations.js
│ │ └── index.js
│ └── GroupItemsExample
│ │ ├── GroupItemsHOC.js
│ │ ├── animations.js
│ │ └── index.js
├── index.css
├── index.js
└── stories
│ ├── ArrayUpdateExampleWrapper.js
│ ├── BasicEnterExitExampleWrapper.js
│ ├── GroupItemsExampleWrapper.js
│ ├── generateItems.js
│ └── index.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "standard",
3 | "plugins": [
4 | "react"
5 | ]
6 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aholachek/react-animations-from-scratch/75192893662c2a180b6492b1e3765251a7e33c1e/.storybook/addons.js
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | require('../src/stories');
5 | }
6 |
7 | configure(loadStories, module);
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [This blog post explains the techniques in more depth](https://medium.com/about-codecademy/building-animations-in-react-from-scratch-c66a582c9b65)
2 |
3 | # Building animations in React from scratch
4 |
5 | ## 1. Basic sequenced enter & exit
6 | 
7 |
8 | - [Live example](http://alex.holachek.com/react-animations-from-scratch/?selectedKind=Animation%20Examples&selectedStory=1.%20The%20basic%20technique&full=0&down=1&left=1&panelRight=0)
9 | - [Code](./src/examples/BasicEnterExitExample)
10 |
11 |
12 | ## 2. Grouping items
13 | 
14 |
15 | - [Live example](http://alex.holachek.com/react-animations-from-scratch/?selectedKind=Animation%20Examples&selectedStory=2.%20Object%20persistence&full=0&down=1&left=1&panelRight=0)
16 | - [Code](./src/examples/GroupItemsExample)
17 |
18 |
19 | ## 3. Enter/update/exit
20 | 
21 |
22 | - [Live example](http://alex.holachek.com/react-animations-from-scratch/?selectedKind=Animation%20Examples&selectedStory=3.%20Enter%2C%20update%20and%20delete%20transitions&full=0&down=1&left=1&panelRight=0)
23 | - [Code](./src/examples/ArrayUpdateExample)
24 |
--------------------------------------------------------------------------------
/gifs/array_update.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aholachek/react-animations-from-scratch/75192893662c2a180b6492b1e3765251a7e33c1e/gifs/array_update.gif
--------------------------------------------------------------------------------
/gifs/basic_enter_exit.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aholachek/react-animations-from-scratch/75192893662c2a180b6492b1e3765251a7e33c1e/gifs/basic_enter_exit.gif
--------------------------------------------------------------------------------
/gifs/group_update.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aholachek/react-animations-from-scratch/75192893662c2a180b6492b1e3765251a7e33c1e/gifs/group_update.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hoc_demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "animejs": "^2.2.0",
7 | "deep-equal": "^1.0.1",
8 | "lodash": "^4.17.4",
9 | "react": "^16.0.0",
10 | "react-dom": "^16.0.0",
11 | "react-scripts": "1.0.14"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test --env=jsdom",
17 | "eject": "react-scripts eject",
18 | "storybook": "start-storybook -p 9009 -s public",
19 | "build-storybook": "build-storybook -s public",
20 | "deploy-storybook": "storybook-to-ghpages"
21 | },
22 | "devDependencies": {
23 | "@storybook/addon-actions": "^3.2.13",
24 | "@storybook/addon-links": "^3.2.13",
25 | "@storybook/react": "^3.2.13",
26 | "@storybook/storybook-deployer": "^2.0.0",
27 | "eslint": "^4.10.0",
28 | "eslint-config-standard": "^10.2.1",
29 | "eslint-plugin-import": "^2.8.0",
30 | "eslint-plugin-node": "^5.2.1",
31 | "eslint-plugin-promise": "^3.6.0",
32 | "eslint-plugin-react": "^7.4.0",
33 | "eslint-plugin-standard": "^3.0.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aholachek/react-animations-from-scratch/75192893662c2a180b6492b1e3765251a7e33c1e/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import AddAnimations from "./HOC/AddAnimations"
3 | import AddArrayAnimations from "./HOC/AddArrayAnimations"
4 | import List from "./List"
5 | import { animateIn, animateOut } from "./List/animations"
6 | import { animateItemsIn, animateItemsOut } from "./Item/animations"
7 | import { compose } from "lodash/fp"
8 |
9 | const ListWithAnimation = compose(
10 | // AddArrayAnimations(animateItemsIn, animateItemsOut),
11 | AddAnimations(animateIn, animateOut)
12 | )(List)
13 |
14 | class App extends Component {
15 | state = { items: 0 }
16 | render() {
17 | return (
18 |
19 |
20 |
21 | {this.state.items ? (
22 |
28 | ) : (
29 |
35 | )}
36 |
37 | {!!this.state.items && (
38 |
39 |
48 |
57 |
58 | )}
59 |
60 |
64 |
65 | )
66 | }
67 | }
68 |
69 | export default App
70 |
--------------------------------------------------------------------------------
/src/components/Item/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Item = ({ item, defaultInvisible }) => {
5 | let className = `item ${defaultInvisible ? 'item__invisible' : ''}`
6 | if (item.shape) className = `${className} item__${item.shape}`
7 | if (item.color) className = `${className} item__${item.color}`
8 | return (
9 |
15 | {item.letter ? item.letter : item.id}
16 |
17 | )
18 | }
19 |
20 | Item.propTypes = {
21 | item: PropTypes.object,
22 | defaultInvisible: PropTypes.bool
23 | }
24 |
25 | export default Item
26 |
--------------------------------------------------------------------------------
/src/components/List/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Item from './../Item'
4 |
5 | // this cannot be a functional component
6 | // as we will need to access its ref from the HOC wrapper
7 | export default class List extends React.Component {
8 | static propTypes = {
9 | items: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
10 | defaultInvisibleItems: PropTypes.bool,
11 | backgroundColor: PropTypes.bool,
12 | // to show that animations can proceed even as other parts of the component
13 | // are getting updated
14 | unrelatedProp: PropTypes.string
15 | }
16 | render() {
17 | const {
18 | items,
19 | defaultInvisibleItems,
20 | backgroundColor,
21 | unrelatedProp
22 | } = this.props
23 |
24 | const unrelatedSection = unrelatedProp ? (
25 |
26 | Unrelated updating props will not interfere with the animation:
27 |
28 | {unrelatedProp}
29 |
30 |
31 | ) : null
32 |
33 | if (Array.isArray(items)) {
34 | return (
35 |
36 | {unrelatedSection}
37 |
38 | {this.props.items.map(item => (
39 |
44 | ))}
45 |
46 |
47 | )
48 | } else {
49 | return (
50 |
51 | {Object.values(items).map(itemGroup => (
52 |
56 | ))}
57 |
58 | )
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/examples/ArrayUpdateExample/AddArrayAnimationsHOC.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | // function used by componentWillReceiveProps and shouldComponentUpdate
5 | // to figure out whether there are entering items and/or exiting items
6 | function getComparisonData(currentProps, nextProps) {
7 | const currentIds = currentProps.items.map(item => item.id)
8 | const nextIds = nextProps.items.map(item => item.id)
9 | const removedIds = currentIds.filter(id => !nextIds.includes(id))
10 | const addedIds = nextIds.filter(id => !currentIds.includes(id))
11 | const persistentIds = nextIds.filter(id => currentIds.includes(id))
12 | return {
13 | removedIds,
14 | addedIds,
15 | persistentIds
16 | }
17 | }
18 |
19 | export default function addArrayAnimations(
20 | animateItemsIn,
21 | animateItemsOut,
22 | animatePersistentItems
23 | ) {
24 | return function wrapComponent(WrappedComponent) {
25 | return class AnimationHOC extends Component {
26 | state = {
27 | animatingOutItems: undefined
28 | }
29 |
30 | componentDidMount() {
31 | animateItemsIn(this.child, this.props.items.map(item => item.id), true)
32 | }
33 |
34 | componentWillReceiveProps(nextProps) {
35 | const { removedIds } = getComparisonData(this.props, nextProps)
36 | removedIds.length &&
37 | this.setState({ animatingOutItems: this.props.items })
38 | }
39 |
40 | shouldComponentUpdate(nextProps, nextState) {
41 | const { removedIds, addedIds, persistentIds } = getComparisonData(
42 | this.props,
43 | nextProps
44 | )
45 |
46 | if (removedIds.length || addedIds.length) {
47 | // preload this function with previous item positions
48 | const animatePersistentPositions = animatePersistentItems(
49 | this.child,
50 | persistentIds
51 | )
52 |
53 | animateItemsOut(this.child, removedIds).then(() => {
54 | // this function will be called by componentDidUpdate as soon as we set
55 | // animatingOutItems state to undefined
56 | this._updateAnimation = () => {
57 | animatePersistentPositions().then(() =>
58 | animateItemsIn(this.child, addedIds)
59 | )
60 | }
61 | this.setState({ animatingOutItems: undefined })
62 | })
63 | return false
64 | } else {
65 | return true
66 | }
67 | }
68 |
69 | componentDidUpdate(prevProps, prevState) {
70 | if (prevState.animatingOutItems && !this.state.animatingOutItems) {
71 | this._updateAnimation()
72 | delete this._updateAnimation
73 | }
74 | }
75 |
76 | render() {
77 | const getRef = component => {
78 | return component && (this.child = ReactDOM.findDOMNode(component))
79 | }
80 | const { items, ...passThroughProps } = this.props
81 |
82 | return (
83 |
89 | )
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/examples/ArrayUpdateExample/animations.js:
--------------------------------------------------------------------------------
1 | import anime from 'animejs'
2 |
3 | const duration = 1000
4 | const delay = (el, i) => i * 100
5 |
6 | // store reference to current animation
7 | let currentAnimation
8 | // in case updates are coming in while another animation is still in progress,
9 | // just fast forward the curent animation to finish it up
10 | const fastForwardCurrentAnimation = () => {
11 | if (currentAnimation) currentAnimation.seek(currentAnimation.duration)
12 | }
13 |
14 | const getItems = (container, ids) => {
15 | return [...container.querySelectorAll('.item')].filter(c => {
16 | return ids.includes(parseInt(c.dataset.id))
17 | })
18 | }
19 | export const animateItemsOut = (ListContainer, removedIds) => {
20 | fastForwardCurrentAnimation()
21 | const removedItems = getItems(ListContainer, removedIds)
22 | if (!removedItems.length) return new Promise(resolve => resolve())
23 |
24 | currentAnimation = anime({
25 | targets: removedItems,
26 | opacity: 0,
27 | scale: 0,
28 | duration: duration/2,
29 | delay
30 | })
31 | return currentAnimation.finished
32 | }
33 |
34 | export const animatePersistentItems = (ListContainer, persistentIds) => {
35 | const persistentItems = getItems(ListContainer, persistentIds)
36 | const oldPositionDict = persistentItems.reduce((acc, Item) => {
37 | acc[Item.dataset.id] = Item.getBoundingClientRect()
38 | return acc
39 | }, {})
40 | return function () {
41 | fastForwardCurrentAnimation()
42 | const persistentItems = getItems(ListContainer, persistentIds)
43 | if (!persistentItems.length) return new Promise(resolve => resolve())
44 |
45 | const transformPositionDict = {}
46 | const targets = persistentItems.filter(item => {
47 | const oldPosition = oldPositionDict[item.dataset.id]
48 | // animations might be cycling through rapidly, so just ignore this node
49 | if (!oldPosition) return false
50 | const newPosition = item.getBoundingClientRect()
51 | const translateX = oldPosition.left - newPosition.left
52 | const translateY = oldPosition.top - newPosition.top
53 | item.style.transform = `translate(${translateX}px, ${translateY}px)`
54 | transformPositionDict[item.dataset.id] = {
55 | translateX: [translateX, 0],
56 | translateY: [translateY, 0]
57 | }
58 | return true
59 | })
60 |
61 | currentAnimation = anime({
62 | targets,
63 | translateX: Item => transformPositionDict[Item.dataset.id].translateX,
64 | translateY: Item => transformPositionDict[Item.dataset.id].translateY,
65 | opacity: 1,
66 | duration,
67 | delay
68 | })
69 | return currentAnimation.finished
70 | }
71 | }
72 |
73 | export const animateItemsIn = (ListContainer, newIds, skipAnimation) => {
74 | fastForwardCurrentAnimation()
75 | const newItems = getItems(ListContainer, newIds)
76 |
77 | currentAnimation = anime({
78 | targets: newItems,
79 | opacity: [0, 1],
80 | scale: [0, 1],
81 | duration: skipAnimation ? 0 : duration / 2,
82 | delay
83 | })
84 | return currentAnimation.finished
85 | }
86 |
--------------------------------------------------------------------------------
/src/examples/ArrayUpdateExample/index.js:
--------------------------------------------------------------------------------
1 | import List from './../../components/List'
2 | import AddArrayAnimationsHOC from './AddArrayAnimationsHOC'
3 | import {
4 | animateItemsIn,
5 | animateItemsOut,
6 | animatePersistentItems
7 | } from './animations'
8 |
9 | export default AddArrayAnimationsHOC(
10 | animateItemsIn,
11 | animateItemsOut,
12 | animatePersistentItems
13 | )(List)
14 |
--------------------------------------------------------------------------------
/src/examples/BasicEnterExitExample/AddAnimationsHOC.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | export default function addAnimation(animateIn, animateOut) {
5 | return function wrapComponent(WrappedComponent) {
6 | return class AnimationHOC extends Component {
7 |
8 | state = { animatingOut: false }
9 |
10 | componentDidMount() {
11 | if (this.props.isVisible) animateIn(this.child)
12 | }
13 |
14 | componentWillReceiveProps(nextProps) {
15 | if (this.props.isVisible && !nextProps.isVisible) {
16 | this.setState({ animatingOut: true })
17 | }
18 | }
19 |
20 | shouldComponentUpdate(nextProps) {
21 | if (this.props.isVisible && !nextProps.isVisible) {
22 | animateOut(this.child, () => {
23 | this.setState({ animatingOut: false })
24 | })
25 | return false
26 | }
27 | return true
28 | }
29 |
30 | componentDidUpdate(prevProps) {
31 | if (!prevProps.isVisible && this.props.isVisible) {
32 | animateIn(this.child)
33 | }
34 | }
35 |
36 | render() {
37 | const { isVisible, ...rest } = this.props
38 | const getRef = component => {
39 | return component && (this.child = ReactDOM.findDOMNode(component))
40 | }
41 | return (
42 | (!!isVisible || this.state.animatingOut) && (
43 |
44 | )
45 | )
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/examples/BasicEnterExitExample/animations.js:
--------------------------------------------------------------------------------
1 | import anime from 'animejs'
2 |
3 | let currentAnimation
4 |
5 | export const animateIn = ListContainer => {
6 | if (currentAnimation) currentAnimation.pause()
7 | const items = [...ListContainer.querySelectorAll('.item')]
8 | items.forEach(c => {
9 | c.style.opacity = 0
10 | })
11 | currentAnimation = anime
12 | .timeline()
13 | .add({
14 | targets: ListContainer,
15 | translateX: [-1000, 0],
16 | opacity: [0, 1],
17 | duration: 500,
18 | elasticity: 100
19 | })
20 | .add({
21 | targets: items,
22 | duration: 400,
23 | opacity: [0, 1],
24 | translateY: [-30, 0],
25 | delay: (el, i) => i * 75
26 | })
27 | }
28 |
29 | export const animateOut = (ListContainer, callback) => {
30 | if (currentAnimation) currentAnimation.pause()
31 | const items = ListContainer.querySelectorAll('.item')
32 | currentAnimation = anime
33 | .timeline()
34 | .add({
35 | targets: items,
36 | duration: 100,
37 | opacity: 0,
38 | translateY: -30,
39 | delay: (el, i) => i * 50,
40 | easing: 'easeInOutSine'
41 | })
42 | .add({
43 | targets: ListContainer,
44 | translateX: 1000,
45 | opacity: [1, 0],
46 | duration: 1000,
47 | complete: callback,
48 | elasticity: 100
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/src/examples/BasicEnterExitExample/index.js:
--------------------------------------------------------------------------------
1 | import List from './../../components/List'
2 | import AddAnimationsHOC from './AddAnimationsHOC'
3 | import { animateIn, animateOut } from './animations'
4 |
5 | export default AddAnimationsHOC(
6 | animateIn,
7 | animateOut
8 | )(List)
9 |
--------------------------------------------------------------------------------
/src/examples/GroupItemsExample/GroupItemsHOC.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | export default function addArrayAnimations (animateGroups) {
5 | return function wrapComponent (WrappedComponent) {
6 | return class AnimationHOC extends Component {
7 | componentWillReceiveProps (newProps) {
8 | if (newProps.group !== this.props.group) {
9 | this._initiateAnimation = animateGroups(this.child)
10 | } else {
11 | delete this._initiateAnimation
12 | }
13 | }
14 |
15 | componentDidUpdate () {
16 | this._initiateAnimation && this._initiateAnimation()
17 | }
18 |
19 | render () {
20 | const getRef = component =>
21 | component && (this.child = ReactDOM.findDOMNode(component))
22 | return
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/examples/GroupItemsExample/animations.js:
--------------------------------------------------------------------------------
1 | import anime from 'animejs'
2 |
3 | export function animateGroups (ListComponent) {
4 | const items = [...ListComponent.querySelectorAll('.item')]
5 | const oldPositionDict = items.reduce((acc, item) => {
6 | acc[item.dataset.id] = item.getBoundingClientRect()
7 | return acc
8 | }, {})
9 | return function initiateAnimation () {
10 | const transformPositionDict = {}
11 | // make sure to get the new array -- React might have destroyed
12 | // and created new DOM nodes
13 | const items = [...ListComponent.querySelectorAll('.item')]
14 | items.forEach(item => {
15 | const oldPosition = oldPositionDict[item.dataset.id]
16 | const newPosition = item.getBoundingClientRect()
17 | const translateX = oldPosition.left - newPosition.left
18 | const translateY = oldPosition.top - newPosition.top
19 | item.style.transform = `translate(${translateX}px, ${translateY}px)`
20 | transformPositionDict[item.dataset.id] = {
21 | translateX: [translateX, 0],
22 | translateY: [translateY, 0]
23 | }
24 | })
25 | anime({
26 | targets: items,
27 | translateX: item => transformPositionDict[item.dataset.id].translateX,
28 | translateY: item => transformPositionDict[item.dataset.id].translateY,
29 | duration: 1000,
30 | delay: (item, i) => i * 12,
31 | easing: 'easeInOutElastic',
32 | elasticity: 1
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/examples/GroupItemsExample/index.js:
--------------------------------------------------------------------------------
1 | import List from './../../components/List'
2 | import GroupItemsHOC from './GroupItemsHOC'
3 | import { animateGroups } from './animations'
4 |
5 | export default GroupItemsHOC(
6 | animateGroups
7 | )(List)
8 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI",
6 | "Roboto", "Helvetica Neue", Arial, sans-serif;
7 | line-height: 1.5;
8 | }
9 |
10 | .App {
11 | max-width: 593px;
12 | margin: 3rem 0 0 5rem;
13 | }
14 |
15 | .item {
16 | margin-right: 1rem;
17 | margin-bottom: 1rem;
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | width: 3rem;
22 | height: 3rem;
23 | border: 0;
24 | border-radius: 0;
25 | transition: background-color 0.2s ease-in-out, border-radius 0.2s ease-in-out;
26 | color: white;
27 | }
28 |
29 | /* sometimes entering items should be invisible until they have an enter animation applied */
30 | .item__invisible {
31 | opacity: 0;
32 | }
33 |
34 | .item__blue {
35 | background-color: #3185fc;
36 | }
37 |
38 | .item__red {
39 | background-color: #ee4266;
40 | }
41 |
42 | .item__green {
43 | background-color: #59c9a5;
44 | }
45 |
46 | .item__circle {
47 | border-radius: 100%;
48 | }
49 |
50 | .item__rounded {
51 | border-radius: 0.5rem;
52 | }
53 |
54 | .item__square {
55 | border-radius: 0;
56 | }
57 |
58 | .list {
59 | display: flex;
60 | flex-wrap: wrap;
61 | align-items: center;
62 | padding: 0;
63 | margin-bottom: 2rem;
64 | }
65 |
66 | .list--background {
67 | background: rgb(236, 236, 236);
68 | padding: 1rem 0 0 1rem;
69 | padding-right: 0;
70 | }
71 |
72 | .list li {
73 | list-style-type: none;
74 | }
75 |
76 | .list__update-explanation {
77 | margin-top: 1rem;
78 | }
79 |
80 | button {
81 | padding: 0.5rem 1rem;
82 | border: 1px solid rgb(185, 185, 185);
83 | font-size: 1rem;
84 | cursor: pointer;
85 | margin-bottom: 1rem;
86 | }
87 |
88 | button:hover,
89 | button:focus {
90 | background-color: rgba(185, 185, 185, 0.3);
91 | }
92 |
93 | label {
94 | margin-right: 1rem;
95 | }
96 |
97 | legend {
98 | font-weight: bold;
99 | }
100 |
101 | fieldset {
102 | border: 0;
103 | padding: 0;
104 | margin: 0;
105 | margin-bottom: 1rem;
106 | }
107 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 |
6 | ReactDOM.render(, document.getElementById('root'))
7 |
--------------------------------------------------------------------------------
/src/stories/ArrayUpdateExampleWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ListWithAnimation from './../examples/ArrayUpdateExample'
3 | import generateItems from './generateItems'
4 |
5 | function randomTrue(divisor) {
6 | return () => Math.random() <= 1 / divisor
7 | }
8 |
9 | const items = generateItems(75)
10 |
11 | class App extends Component {
12 | state = {
13 | items: items.filter(randomTrue(3)),
14 | unrelatedProp: Math.random().toFixed(5)
15 | }
16 |
17 | componentDidMount() {
18 | this.intervalId = setInterval(() => {
19 | this.setState({ unrelatedProp: Math.random().toFixed(5) })
20 | }, 500)
21 | }
22 | componentWillUnmount() {
23 | this.intervalId && clearInterval(this.intervalId)
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
40 |
41 |
45 |
46 | )
47 | }
48 | }
49 |
50 | export default App
51 |
--------------------------------------------------------------------------------
/src/stories/BasicEnterExitExampleWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ListWithAnimation from './../examples/BasicEnterExitExample'
3 | import generateItems from './generateItems'
4 |
5 | class App extends Component {
6 | state = {
7 | items: generateItems(27),
8 | isVisible: false
9 | }
10 | componentDidMount() {
11 | this.intervalId = setInterval(() => {
12 | this.setState({ items: generateItems(27) })
13 | }, 750)
14 | }
15 | componentWillUnmount() {
16 | this.intervalId && clearInterval(this.intervalId)
17 | }
18 | render() {
19 | return (
20 |
21 |
22 | Item attributes are continually updating to show that the
23 | animation proceeds as normal even when children are being updated.
24 |
25 |
26 | {this.state.isVisible ? (
27 |
30 | ) : (
31 |
34 | )}
35 |
36 |
37 |
42 |
43 | )
44 | }
45 | }
46 |
47 | export default App
48 |
--------------------------------------------------------------------------------
/src/stories/GroupItemsExampleWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ListWithAnimation from './../examples/GroupItemsExample'
3 | import generateItems from './generateItems'
4 |
5 | const groups = ['all', 'color', 'shape', 'even/odd']
6 |
7 | class App extends Component {
8 | state = {
9 | items: generateItems(),
10 | group: 'all'
11 | }
12 | render() {
13 | const { items, group } = this.state
14 | let groupedItems = {}
15 | items.forEach(item => {
16 | if (groupedItems[item[group]]) groupedItems[item[group]].push(item)
17 | else groupedItems[item[group]] = [item]
18 | })
19 | return (
20 |
21 |
22 |
41 |
42 |
43 |
44 | )
45 | }
46 | }
47 |
48 | export default App
49 |
--------------------------------------------------------------------------------
/src/stories/generateItems.js:
--------------------------------------------------------------------------------
1 | const getRandomFromList = arr => arr[Math.floor(Math.random() * arr.length)]
2 |
3 | const colors = ['red', 'blue', 'green']
4 | const shapes = ['square', 'circle', 'rounded']
5 |
6 | export default function generateItems (len = 50) {
7 | const items = [...Array(len + 1).keys()].slice(1)
8 | return items.map(item => ({
9 | id: item,
10 | color: getRandomFromList(colors),
11 | shape: getRandomFromList(shapes),
12 | 'even/odd': item % 2 === 0
13 | }))
14 | }
15 |
--------------------------------------------------------------------------------
/src/stories/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import './../index.css'
5 |
6 | import BasicEnterExitExample from './BasicEnterExitExampleWrapper'
7 | import ArrayUpdateExample from './ArrayUpdateExampleWrapper'
8 | import GroupItemsExample from './GroupItemsExampleWrapper'
9 |
10 | storiesOf('Animation Examples', module)
11 | .add('1. The basic technique', () => )
12 | .add('2. Object persistence', () => )
13 | .add('3. Enter, update and delete transitions', () => )
14 |
--------------------------------------------------------------------------------