├── .gitignore ├── Readme.md ├── docs ├── grid.gif ├── grid.mov ├── photojump.gif └── photojump.mov ├── example.js ├── index.html ├── index.js ├── package.json ├── photojump.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | /node_modules 4 | /build 5 | *.swo 6 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # teleporter 2 | 3 | Teleport components around the tree without losing state or destroying the 4 | DOM! 5 | 6 | [some context/discussion in this gist](https://gist.github.com/chenglou/34b155691a6f58091953) 7 | 8 | ```javascript 9 | import {teleportable, teleparent} from 'react-teleport' 10 | ``` 11 | 12 | ## Examples 13 | 14 | ### [Basic Grid](docs/grid.mov) 15 | 16 | [read the source](./example.js), interesing lines identified with `/***\ <---- \***/` 17 | 18 | (click image for .mov) 19 | 20 | [![grid](docs/grid.gif)](docs/grid.mov) 21 | 22 | ### [Photo list](docs/photojump.mov) 23 | 24 | [read the source](./photojump.js), interesing lines identified with `/***\ <---- \***/` 25 | 26 | (click image for .mov) 27 | 28 | [![image](docs/photojump.gif)](docs/photojump.mov) 29 | 30 | ## API 31 | 32 | ### `@teleportable` 33 | 34 | ```javascript 35 | /* 36 | * This function is best used as a decorator, and it makes a component "teleportable". 37 | * 38 | * A teleportable component can be moved between parents, nodes, etc. without 39 | * losing state, and without losing the DOM tree. 40 | * 41 | * Props: 42 | * - telekey: a teleport key. `teleparents` can create teleport keys. 43 | * 44 | * When the `teleparent` that created a telekey is garbage collected 45 | * (unmounted from the dom), then this teleportable component will also be 46 | * unmounted, but *not until then*. 47 | * 48 | * So if you have a long-lived teleparent with lots of teleportable children, 49 | * you could end up with a fair amount of garbage. 50 | */ 51 | ``` 52 | 53 | ### `@teleparent` 54 | 55 | ```javascript 56 | /** 57 | * Also a @decorator. Makes a component into a `teleparent`. 58 | * 59 | * Teleparents can create telekeys (the unique ids used to manage teleportable 60 | * components), via two functions, given as props: 61 | * 62 | * - makeTelekey() -> a new telekey 63 | * - getTelekey(id) -> get (or create if needed) the telekey corresponding to 64 | * some id 65 | * 66 | * If you only have one or two teleportable components, then `makeTelekey` 67 | * probably makes the most sense. 68 | * 69 | * If you have a bunch of components that need to be teleportable, that are 70 | * identified by some string id already, you can use `getTelekey(id)` to 71 | * always get the same `telekey` for a given `id`. 72 | * 73 | * See `photojump.js` for an example of using `getTelekey`, and `example.js` 74 | * for a simple example of using `makeTelekey`. 75 | */ 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /docs/grid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredly/react-teleporter/8e395690c2758fcab4dc4d97787fdb7938cea542/docs/grid.gif -------------------------------------------------------------------------------- /docs/grid.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredly/react-teleporter/8e395690c2758fcab4dc4d97787fdb7938cea542/docs/grid.mov -------------------------------------------------------------------------------- /docs/photojump.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredly/react-teleporter/8e395690c2758fcab4dc4d97787fdb7938cea542/docs/photojump.gif -------------------------------------------------------------------------------- /docs/photojump.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredly/react-teleporter/8e395690c2758fcab4dc4d97787fdb7938cea542/docs/photojump.mov -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | import {teleportable, teleparent} from './' 5 | 6 | const ten = [] 7 | for (let i=0; i<10; i++) ten.push(i) 8 | 9 | @teleportable /***\ <---- \***/ 10 | class AwesomeItem extends React.Component { 11 | constructor(props) { 12 | super(props) 13 | this.state = {presses: 0} 14 | } 15 | 16 | componentDidMount() { 17 | console.log('Mouting!') 18 | } 19 | 20 | render() { 21 | return
28 | {this.props.initialText}
29 | 33 |
34 | } 35 | } 36 | 37 | function rnd(max) { 38 | return parseInt(Math.random() * max) 39 | } 40 | 41 | @teleparent /***\ <---- \***/ 42 | class Something extends React.Component { 43 | constructor(props) { 44 | super(props) 45 | this.state = {sel: [0, 0]} 46 | } 47 | componentWillMount() { 48 | this._telekey = this.props.makeTelekey() /***\ <---- \***/ 49 | } 50 | rnd() { 51 | this.setState({ 52 | sel: [rnd(5), rnd(10)] 53 | }) 54 | } 55 | 56 | render() { 57 | const sel = this.state.sel 58 | return
59 | 60 | {ten.slice(0, 5).map(a =>
{ten.map(b =>
69 | {a === sel[0] && b === sel[1] ? : null} 73 |
)}
)} 74 |
75 | } 76 | } 77 | 78 | const node = document.createElement('div') 79 | document.body.appendChild(node) 80 | React.render(, node) 81 | 82 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const PT = React.PropTypes 5 | 6 | /** 7 | * This function is best used as a decorator, and it makes a component "teleportable". 8 | * 9 | * A teleportable component can be moved between parents, nodes, etc. without 10 | * losing state, and without losing the DOM tree. 11 | * 12 | * Props: 13 | * - telekey: a teleport key. `teleparents` can create teleport keys. 14 | * 15 | * When the `teleparent` that created a telekey is garbage collected 16 | * (unmounted from the dom), then this teleportable component will also be 17 | * unmounted, but *not until then*. 18 | * 19 | * So if you have a long-lived teleparent with lots of teleportable children, 20 | * you could end up with a fair amount of garbage. 21 | */ 22 | const teleportable = Wrapped => class Teleportable extends React.Component { 23 | static propTypes = { 24 | telekey: PT.string, 25 | } 26 | 27 | componentDidMount() { 28 | this._render() 29 | } 30 | 31 | componentDidUpdate() { 32 | this._render() 33 | } 34 | 35 | onSteal() { 36 | // contents have been stolen. Node will be hoisted. 37 | this._telekey = null 38 | } 39 | 40 | _render() { 41 | const node = React.findDOMNode(this) 42 | let container = teleregistry.get(this.props.telekey).node 43 | if (this._telekey !== this.props.telekey) { 44 | teleregistry.steal(this.props.telekey, this.onSteal.bind(this)) 45 | // Teleporting a component over here. 46 | this._telekey = this.props.telekey 47 | } 48 | 49 | if (node.firstChild) { 50 | if (node.firstChild !== container) { 51 | node.replaceChild(container, node.firstChild) 52 | } 53 | } else { 54 | node.appendChild(container) 55 | } 56 | 57 | React.render(, container) 58 | } 59 | 60 | render() { 61 | return
62 | } 63 | } 64 | 65 | /** 66 | * Also a @decorator. Makes a component into a `teleparent`. 67 | * 68 | * Teleparents can create telekeys (the unique ids used to manage teleportable 69 | * components), via two functions, given as props: 70 | * 71 | * - makeTelekey() -> a new telekey 72 | * - getTelekey(id) -> get (or create if needed) the telekey corresponding to 73 | * some id 74 | * 75 | * If you only have one or two teleportable components, then `makeTelekey` 76 | * probably makes the most sense. 77 | * 78 | * If you have a bunch of components that need to be teleportable, that are 79 | * identified by some string id already, you can use `getTelekey(id)` to 80 | * always get the same `telekey` for a given `id`. 81 | * 82 | * See `photojump.js` for an example of using `getTelekey`, and `example.js` 83 | * for a simple example of using `makeTelekey`. 84 | */ 85 | const teleparent = Wrapped => class Teleparent extends React.Component { 86 | constructor(props) { 87 | super(props) 88 | this.keys = [] 89 | this.namedKeys = {} 90 | this._makeKey = this._makeKey.bind(this); 91 | this.getKey = this.getKey.bind(this); 92 | } 93 | componentWillUnMount() { 94 | this.keys.forEach(key => { 95 | React.unmountComponentAtNode(_reg[id]) 96 | _reg[id] = null 97 | }) 98 | Object.keys(this.namedKeys).forEach(name => { 99 | const id = this.namedKeys[name] 100 | React.unmountComponentAtNode(_reg[id]) 101 | _reg[id] = null 102 | }) 103 | this.namedKeys = {} 104 | this.keys = [] 105 | } 106 | 107 | _makeKey() { 108 | const key = newKey() 109 | this.keys.push(key) 110 | return key 111 | } 112 | getKey(id) { 113 | if (!this.namedKeys[id]) { 114 | this.namedKeys[id] = newKey() 115 | } 116 | return this.namedKeys[id] 117 | } 118 | render() { 119 | return 123 | } 124 | } 125 | 126 | export {teleportable, teleparent} 127 | 128 | /**** internal ****/ 129 | 130 | /** teleregistry, where root containers are tracked. 131 | * 132 | * _reg is a map of telekey => { 133 | * node: DOM node container, 134 | * onSteal: function to call when another component steals this node 135 | * } 136 | */ 137 | 138 | const _reg = {} 139 | 140 | const teleregistry = { 141 | get(id) { 142 | if (!_reg[id]) { 143 | if (_reg[id] === null) { 144 | throw new Error('Using a stale id! The contents have been garbage collected') 145 | } 146 | _reg[id] = { 147 | node: document.createElement('div'), 148 | onSteal: () => {} 149 | } 150 | } 151 | return _reg[id] 152 | }, 153 | 154 | steal(id, onSteal) { 155 | _reg[id].onSteal() 156 | _reg[id].onSteal = onSteal 157 | } 158 | } 159 | 160 | function newKey() { 161 | return Math.random().toString(36).slice(2) 162 | } 163 | 164 | 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teleporter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "react": "^0.13.3" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^5.4.7", 16 | "babel-loader": "^5.1.3", 17 | "json-loader": "^0.5.2", 18 | "node-libs-browser": "^0.5.2", 19 | "webpack": "^1.9.10" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /photojump.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import {teleportable, teleparent} from './' 4 | 5 | const PT = React.PropTypes 6 | 7 | @teleportable /***\ <---- \***/ 8 | class Image extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = {clicks: 0} 12 | } 13 | render() { 14 | return
21 |

Image {this.props.id}

22 | 25 |
26 | } 27 | } 28 | 29 | @teleparent /***\ <---- \***/ 30 | class ImageParent extends React.Component { 31 | constructor(props) { 32 | super(props) 33 | this.state = {popping: null} 34 | } 35 | 36 | static childContextTypes = { 37 | popImage: PT.func, 38 | popKey: PT.func, 39 | } 40 | 41 | getChildContext() { 42 | return { 43 | popImage: id => this.setState({popping: id}), 44 | popKey: id => this.props.getTelekey(id), /***\ <---- \***/ 45 | } 46 | } 47 | 48 | render() { 49 | const style = { 50 | margin: '0 100px', 51 | width: 200 52 | } 53 | if (this.state.popping) { 54 | return
55 | 59 | 62 |
63 | } 64 | return
65 | 66 |
67 | } 68 | } 69 | 70 | const imageIds = ['image1', 'image2', 'image3', 'image4', 'image5'] 71 | 72 | class ImageList extends React.Component { 73 | static contextTypes = { 74 | popImage: PT.func, 75 | popKey: PT.func, 76 | } 77 | 78 | render() { 79 | return
    80 | {imageIds.map(id =>
  • 81 | 83 | 84 |
  • )} 85 |
86 | } 87 | } 88 | 89 | const node = document.createElement('div') 90 | document.body.appendChild(node) 91 | React.render(, node) 92 | 93 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var BASE = path.join(__dirname, 'node_modules') 4 | 5 | module.exports = { 6 | devtool: 'eval', 7 | entry: { 8 | example: './example', 9 | photo: './photojump', 10 | }, 11 | output: { 12 | path: path.join(__dirname, 'build'), 13 | filename: '[name].js', 14 | publicPath: '/app/' 15 | }, 16 | 17 | node: { 18 | fs: 'empty', 19 | net: 'empty', 20 | }, 21 | 22 | module: { 23 | loaders: [{ 24 | test: /\.jsx?$/, 25 | loader: BASE + '/babel-loader?stage=0', 26 | exclude: 'node_modules', 27 | }] 28 | } 29 | }; 30 | --------------------------------------------------------------------------------