Skip to content

Commit 142fb19

Browse files
committed
Big bang 💥
0 parents  commit 142fb19

File tree

15 files changed

+4853
-0
lines changed

15 files changed

+4853
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
lib/

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"trailingComma": "all",
3+
"singleQuote": true,
4+
"semi": false
5+
}

.travis.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
language: node_js
2+
sudo: false
3+
cache:
4+
directories:
5+
- node_modules
6+
node_js:
7+
- "4"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2018 Javier Velasco
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.

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# React Tunnels 🚇 [![npm](https://img.shields.io/npm/v/react-tunnels.svg?style=flat)](https://www.npmjs.org/package/react-tunnels)
2+
3+
Render React components in placeholders that are placed somewhere else in the component tree.
4+
5+
## Install
6+
7+
```
8+
yarn add react-tunnels
9+
```
10+
11+
### Why
12+
13+
There is a common use case in React apps where you want to define a `Layout` where the content of some elements are defined by child components. For example, you want define `Layout` just once and reuse it for every page but it has a breadcrumb whose steps depend on children components. This tiny library allows you to define "tunnels" to render from a element to whatever other element in the app, even elements on top of the tree. It's like `Portal` but the target is a component instead of a DOM element.
14+
15+
### Usage
16+
17+
Define a `TunnelPlaceholder` identified by an `id` and decide what properties are going to be passed to its render function by defining `Tunnel` components with the same id anywhere else in the app. If you define just a single `Tunnel` its props will be passed, if there are more than once tunnel for an `id`, the tunnel will receive an array of `props`. Let's see some examples.
18+
19+
### Simple example: tunneling children
20+
21+
Define a placeholder without any render function so it will render any children coming from `Tunnel` components.
22+
23+
```js
24+
import { TunnelsProvider, TunnelPlaceholder, Tunnel } from 'preact-slots'
25+
26+
render(
27+
<TunnelsProvider>
28+
<div>
29+
<TunnelPlaceholder id="my-tunnel" />
30+
<Tunnel id="my-tunnel">
31+
This will be rendered on the placeholder 👆
32+
</Tunnel>
33+
</div>
34+
</TunnelsProvider>
35+
)
36+
```
37+
38+
### A more complex example: building a Breadcrumb
39+
40+
```js
41+
render(
42+
<TunnelsProvider>
43+
{/* This will render the breadcrumb */}
44+
<Breadcrumbs />
45+
{/* Somewhere else in children */}
46+
<Breadcrumb url="/products">Products</Breadcrumb>
47+
<Breadcrumb url="/products/123">Product <strong>123</strong></Breadcrumb>
48+
</TunnelsProvider>
49+
)
50+
51+
const Breadcrumbs = () => (
52+
<TunnelPlaceholder id="breadcrumb">
53+
{({ items = [] }) => (
54+
<ul>
55+
{items.map(({ children, href }) => (
56+
<li><a href={href}>{children}</a></li>
57+
))}
58+
</ul>
59+
)}
60+
</TunnelPlaceholder>
61+
)
62+
63+
const Breadcrumb = ({ children, url }) => (
64+
<Tunnel id="breadcrumb" url={url}>
65+
{children}
66+
</Tunnel>
67+
)
68+
```
69+
70+
### Similar Libraries
71+
72+
- [React Slot Fill](https://github.com/camwest/react-slot-fill): [@camwest](https://github.com/camwest) has built a similar project but with a different API and a bit more limited use cases. The main difference is that you can't pass content to a placeholder from multiple entry points while react-tunnels does it by passing an array with the props defined by each tunnel to the render function of the placeholder. For simple cases, it is pretty similar.
73+
- [React Slots](https://github.com/developit/preact-slots): A library similar to React Slot Fill but for [Preact](https://github.com/developit/preact) developed by [Jason Miller](https://twitter.com/_developit).
74+
75+
## License
76+
77+
This project is licensed under the terms of the [MIT license](https://github.com/javivelasco/react-tunnels/blob/master/LICENSE).

package.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"name": "react-tunnels",
3+
"version": "1.0.0",
4+
"description": "A easy way to communicate rendering logic and data to ancestor components in React.",
5+
"main": "./lib/index.js",
6+
"scripts": {
7+
"build": "babel ./src --out-dir ./lib",
8+
"build:watch": "babel ./src --out-dir ./lib --watch",
9+
"lint": "eslint src tests",
10+
"prettier": "prettier --write \"{src,tests}/**/*.js\"",
11+
"prepublish": "rimraf lib && yarn build",
12+
"test": "jest"
13+
},
14+
"keywords": [
15+
"react",
16+
"react-tunnels",
17+
"portals",
18+
"components"
19+
],
20+
"author": "Javi Velasco <[email protected]> (http://javivelasco.com/)",
21+
"license": "MIT",
22+
"devDependencies": {
23+
"babel-cli": "^6.26.0",
24+
"babel-core": "^6.26.0",
25+
"babel-eslint": "^8.2.1",
26+
"babel-jest": "^22.1.0",
27+
"babel-preset-env": "^1.6.1",
28+
"babel-preset-react": "^6.24.1",
29+
"babel-preset-stage-2": "^6.24.1",
30+
"enzyme": "^3.3.0",
31+
"enzyme-adapter-react-16": "^1.1.1",
32+
"eslint": "^4.16.0",
33+
"eslint-config-prettier": "^2.9.0",
34+
"eslint-plugin-import": "^2.8.0",
35+
"eslint-plugin-prettier": "^2.5.0",
36+
"eslint-plugin-react": "^7.6.1",
37+
"jest": "^22.1.4",
38+
"prettier": "^1.10.2",
39+
"prop-types": "^15.6.0",
40+
"react": "^16.2.0",
41+
"react-dom": "^16.2.0",
42+
"rimraf": "^2.6.2"
43+
},
44+
"peerDependencies": {
45+
"react": "^0.14.9 || ^15.3.0 || ^16.0.0"
46+
},
47+
"babel": {
48+
"presets": [
49+
"env",
50+
"stage-2",
51+
"react"
52+
]
53+
},
54+
"eslintConfig": {
55+
"env": {
56+
"browser": true,
57+
"node": true
58+
},
59+
"parserOptions": {
60+
"ecmaVersion": 7,
61+
"sourceType": "module",
62+
"ecmaFeatures": {
63+
"jsx": true
64+
}
65+
},
66+
"parser": "babel-eslint",
67+
"extends": [
68+
"plugin:react/recommended"
69+
],
70+
"rules": {
71+
"semi": 0,
72+
"quotes": 0,
73+
"comma-dangle": 0,
74+
"curly": [2, "multi-line"],
75+
"arrow-parens": 0,
76+
"class-methods-use-this": 0,
77+
"symbol-description": 0,
78+
"no-unused-vars": [2, { "varsIgnorePattern": "^_+$" }],
79+
"import/no-extraneous-dependencies": 0,
80+
"no-confusing-arrow": 0,
81+
"no-else-return": 0,
82+
"react/sort-comp": 0,
83+
"react/jsx-filename-extension": 0,
84+
"no-prototype-builtins": 0,
85+
"no-duplicate-imports": 0,
86+
"prettier/prettier": 2
87+
},
88+
"plugins": [
89+
"prettier",
90+
"react"
91+
]
92+
}
93+
}

src/Tunnel.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Component } from 'react'
2+
import PropTypes from 'prop-types'
3+
import uniqueId from './uniqueId'
4+
5+
class Tunnel extends Component {
6+
static propTypes = {
7+
id: PropTypes.string,
8+
render: PropTypes.func,
9+
}
10+
11+
static contextTypes = {
12+
tunnelState: PropTypes.object,
13+
}
14+
15+
itemId = uniqueId()
16+
17+
componentDidMount() {
18+
this.setTunnelProps(this.props)
19+
}
20+
21+
componentWillUpdate(nextProps) {
22+
this.setTunnelProps(nextProps)
23+
}
24+
25+
componentWillUnmount() {
26+
const { id } = this.props
27+
const { tunnelState } = this.context
28+
tunnelState.setTunnelProps(id, this.itemId, null)
29+
}
30+
31+
setTunnelProps(newProps) {
32+
const { id, ...props } = newProps
33+
const { tunnelState } = this.context
34+
tunnelState.setTunnelProps(id, this.itemId, props)
35+
}
36+
37+
render() {
38+
return null
39+
}
40+
}
41+
42+
export default Tunnel

src/TunnelPlaceholder.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import PropTypes from 'prop-types'
2+
import React, { createElement, Component, Fragment } from 'react'
3+
4+
class TunnelPlaceholder extends Component {
5+
static propTypes = {
6+
children: PropTypes.func,
7+
id: PropTypes.string.isRequired,
8+
}
9+
10+
static contextTypes = {
11+
tunnelState: PropTypes.object,
12+
}
13+
14+
componentDidMount() {
15+
const { id } = this.props
16+
const { tunnelState } = this.context
17+
tunnelState.subscribe(id, this.handlePropsChange)
18+
}
19+
20+
componentWillUnmount() {
21+
const { id } = this.props
22+
const { tunnelState } = this.context
23+
tunnelState.unsubscribe(id, this.handlePropsChange)
24+
}
25+
26+
handlePropsChange = () => {
27+
this.forceUpdate()
28+
}
29+
30+
render() {
31+
const { tunnelState } = this.context
32+
const { id, children: renderChildren } = this.props
33+
const tunnelProps = tunnelState.getTunnelProps(id)
34+
35+
if (Array.isArray(tunnelProps)) {
36+
if (renderChildren) {
37+
return createElement(renderChildren, { items: tunnelProps })
38+
}
39+
40+
if (tunnelProps.length > 0) {
41+
return <Fragment>{tunnelProps.map(props => props.children)}</Fragment>
42+
}
43+
} else {
44+
if (renderChildren) {
45+
return createElement(renderChildren, tunnelProps || {})
46+
}
47+
48+
if (!tunnelProps) {
49+
return null
50+
}
51+
52+
return <Fragment>{tunnelProps.children}</Fragment>
53+
}
54+
55+
return null
56+
}
57+
}
58+
59+
export default TunnelPlaceholder

src/TunnelProvider.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import PropTypes from 'prop-types'
2+
import { Children, Component } from 'react'
3+
import TunnelState from './TunnelState'
4+
5+
class TunnelProvider extends Component {
6+
static propTypes = {
7+
children: PropTypes.node,
8+
}
9+
10+
static childContextTypes = {
11+
tunnelState: PropTypes.object,
12+
}
13+
14+
tunnelState = new TunnelState()
15+
16+
getChildContext() {
17+
return {
18+
tunnelState: this.tunnelState,
19+
}
20+
}
21+
22+
render() {
23+
return Children.only(this.props.children)
24+
}
25+
}
26+
27+
export default TunnelProvider

src/TunnelState.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class TunnelState {
2+
tunnels = {}
3+
listeners = {}
4+
tunnelsDict = {}
5+
6+
getListeners(id) {
7+
return this.listeners[id] || []
8+
}
9+
10+
subscribe(id, fn) {
11+
this.listeners[id] = [...this.getListeners(id), fn]
12+
}
13+
14+
unsubscribe(id, fn) {
15+
this.listeners[id] = this.getListeners(id).filter(
16+
listener => listener !== fn,
17+
)
18+
}
19+
20+
setTunnelProps(id, itemId, props) {
21+
this.tunnels[id] = this.tunnels[id] || []
22+
this.tunnelsDict[id] = this.tunnelsDict[id] || {}
23+
24+
if (props !== null) {
25+
if (!this.tunnelsDict[id][itemId]) {
26+
this.tunnels[id].push(itemId)
27+
}
28+
this.tunnelsDict[id][itemId] = props
29+
} else {
30+
delete this.tunnelsDict[id][itemId]
31+
const idx = this.tunnels[id].indexOf(itemId)
32+
this.tunnels[id] = [
33+
...this.tunnels[id].slice(0, idx),
34+
...this.tunnels[id].slice(idx + 1),
35+
]
36+
}
37+
38+
if (this.listeners[id]) {
39+
this.listeners[id].forEach(fn => fn(props))
40+
}
41+
}
42+
43+
getTunnelProps(id) {
44+
if (this.tunnels[id]) {
45+
return this.tunnels[id].length < 2
46+
? this.tunnelsDict[id][this.tunnels[id][0]]
47+
: this.tunnels[id].map(i => this.tunnelsDict[id][i])
48+
}
49+
50+
return null
51+
}
52+
}
53+
54+
export default TunnelState

0 commit comments

Comments
 (0)