├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── babel.config.js ├── eslint.config.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── conduit-provider.js ├── conduit.js ├── index.js ├── inlet.js ├── outlet.js ├── poor-man-ramda.js └── registry.js └── test ├── __snapshots__ ├── registry.test.js.snap └── snapshots.test.js.snap ├── enzyme-setup.js ├── package.json ├── poor-man-ramda.test.js ├── registry.test.js └── snapshots.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | eslint.config.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log 4 | yarn-error.log 5 | yarn.lock 6 | .node-version 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | - 14 6 | cache: 7 | directories: 8 | - node_modules 9 | deploy: 10 | provider: npm 11 | email: contact@trabesoluciones.com 12 | api_key: 13 | secure: eVc7/fMyQCH6SiDTIZ/2iqhgzMeV/S91Lg4X5Gc/2KZPL1XLkHtOHBYtmZu+eGdnbqBx6+mfT7ArJGSxpv5o5lrLlJYbTvFi4Hj4cSTGf6eyBkZ/VnBISXRMx3E4EoTItIfpx5k6rA/9iGiYkUabUnch7cN09NelPM4FUq795zmj8Qrs62Smjo+Wc0vYwfprW4qVeje8wRVw1w5GHVrKE1xeYHoQUxlWZ1ZDKxAnzWupGRwJesgVZYsx0S8qx+mx9May8ywUZPig0e3Z6QdkQga7L2vcRgF4GOWA+pU3UrjHRhGW5n9G+lGS7mXvh/9glXKJiPL/VUq1Vuhv7Rce6MdHjJOo6cAdk+YKVMBC0QT/PtNCCQLKxlbik/Lz+DRY9aYvBdvD7n7eZyCj6FN+yer1sSAtBMjRazN0SeRCSYaa9yzArJARII4szK1IEiquW+aY0EKurvJ7AGn6+JlkSC3eCpbdWHjlNepFG0cXRHw0NNt1QmjMvkrjksUrzbAXXdptrISp2PuZcPhTOjz15ntK5e8w7wwmBHBrLPANXULm+FUZ1jnM/vtQ68tiRe/CIHmVi1dWXHfD6i+hq4Qv1agPQi5N8mCKs79BfePuUuS+uF/urVGD2ZElgLPIB0z3dCrSbVudI7+Bys+WFlX1WexslK1et+E+YR61jvrscRk= 14 | on: 15 | tags: true 16 | repo: trabe/react-conduit 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-conduit 2 | 3 | [![Build Status](https://travis-ci.org/trabe/react-conduit.svg?branch=master)](https://travis-ci.org/trabe/react-conduit) 4 | 5 | ## Description 6 | 7 | react-conduit allows you to render React components anywhere. Typical use case 8 | scenarios are: 9 | 10 | * Keep shared state in the parent but render the children anywhere. 11 | * Imperative APIs for things like rendering alerts or confirmations in a different react application and send it to your main application. 12 | * Render componentes in z-indexed layers with ease. 13 | 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install --save react-conduit 19 | ``` 20 | 21 | ## Usage 22 | 23 | First and foremost you need to wrap your react componentes with a ``. 24 | After that you use an `` to wrap the components you need to render and drop 25 | an `` wherever you want the components to be rendered. 26 | 27 | ``` 28 | import React from "react"; 29 | import { Inlet, Outlet, ConduitProvider } from "react-conduit"; 30 | 31 | 32 |
33 |

This paragraph will be a .divA child in the DOM

34 | 35 |

This paragraph will be a .divB child in the DOM

36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | ``` 44 | 45 | You can drop several inlets and outlets in your application. Components inside inlets 46 | will be rendered on outlets based on their labels. 47 | 48 | **NOTE**: using the same label in several outputs will replicate the inlet-ed components. 49 | 50 | It's possible to force the order of the elements that are being output at a certain outlet 51 | by passing an `index` prop to the `` (defaults to 0, thus elements are output by registering 52 | in the inlet order). **Negative numbers will be rendered first in the DOM**. 53 | 54 | To further customize the element that wraps the outlet you can use both `className` and `style` 55 | props in the ``. 56 | 57 | ``` 58 | import React from "react"; 59 | import { Inlet, Outlet, ConduitProvider } from "react-conduit"; 60 | 61 | 62 |
63 |

This paragraph will be a .divA child in the DOM

64 | 65 |

This paragraph will be the second .divB child in the DOM

66 |
67 | 68 |

This paragraph will be the first .divB child in the DOM

69 |
70 |
71 | 72 | 73 |
74 | ``` 75 | 76 | 77 | ## API 78 | 79 | ### Inlet 80 | 81 | | Prop | Type | Req? | Description | 82 | | :------------- | :----------- | :----- | :-----------------------------------------------------------------------------| 83 | | label | string | ✓ | Label matching one of the outlets | 84 | | index | integer | | Index for ordering the outlet output. Negative indexes will be rendered first | 85 | | onConnect | func | | Callback invoked when a new conduit is connected | 86 | | onDisconnect | func | | Callback invoked when a conduit gets disconnected | 87 | 88 | ### Outlet 89 | 90 | | Prop | Type | Req? | Description | 91 | |:-------------|:-----------|:-----|:---------------------------------------------------------| 92 | | label | string | ✓ | Outlet identifier | 93 | | className | string | | Additional className for the outlet wrapper | 94 | | style | object | | Additional styles for the outlet wrapper | 95 | | onConnect | func | | Callback invoked when a new conduit is connected | 96 | | onDisconnect | func | | Callback invoked when a conduit gets disconnected | 97 | 98 | ## CHANGELOG 99 | 100 | ### v3.1.0 101 | 102 | * Remove ramda dependency. 103 | 104 | ### v3.0.0 105 | 106 | * [BREAKING] No longer supports React version < 16.8. 107 | * Update dependencies 108 | 109 | ### v2.0.0 110 | 111 | * [BREAKING] No longer supports React version < 16.3. 112 | * Update dependencies. 113 | * Use config files for tooling instead of package.json entries. 114 | 115 | ### v1.2.0 116 | 117 | * Update uuid dependency. 118 | * Fix uuid imports to ease the pain of mocking test with jest. 119 | 120 | ### v1.1.0 121 | 122 | * Updated all dependencies. 123 | * No longer use Sinon and Chai for testing. Just jest. 124 | * Added ESLint + Prettier to make our code nicer. 125 | 126 | ### v1.0.2 127 | 128 | * Support for React 16. 129 | 130 | ### v1.0.1 131 | 132 | * Fix Inlet ordering not working. 133 | 134 | ### v1.0.0 135 | 136 | * Add `onDisconnect` and `onConnect` callbacks to inlets and outlets. 137 | * Big refactor. Docs are on their way! 138 | 139 | ### v0.2.0 140 | 141 | * Add index prop to Inlet to allow reordering at output time. 142 | * Add className and style props to Outlet to customize the wrapper. 143 | 144 | ### v0.1.1 145 | 146 | * Fixed subscription/unsubscription gotchas. 147 | 148 | ### v0.1.0 149 | 150 | * Initial release. 151 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | browsers: ["last 2 versions", "IE >= 10"], 8 | }, 9 | useBuiltIns: false, 10 | }, 11 | ], 12 | "@babel/react", 13 | ], 14 | plugins: ["@babel/plugin-proposal-class-properties"], 15 | }; 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "babel-eslint", 3 | plugins: ["prettier"], 4 | extends: ["unobtrusive", "unobtrusive/react", "prettier", "prettier/react"], 5 | env: { 6 | browser: true, 7 | }, 8 | settings: { 9 | react: { 10 | version: "detect", 11 | }, 12 | }, 13 | rules: { 14 | "prettier/prettier": ["error", require("./prettier.config.js")], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "\\.js$": "babel-jest", 4 | }, 5 | setupFiles: ["/test/enzyme-setup.js"], 6 | snapshotSerializers: ["/node_modules/enzyme-to-json/serializer"], 7 | testURL: "http://localhost/", 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-conduit", 3 | "version": "3.1.0", 4 | "description": "Place components anywhere in your DOM tree", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "README.md" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/trabe/react-conduit.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/trabe/react-conduit/issues" 16 | }, 17 | "homepage": "https://github.com/trabe/react-conduit", 18 | "author": "Roman Coedo ", 19 | "contributors": [ 20 | "David Barral ", 21 | "Asís García " 22 | ], 23 | "license": "MIT", 24 | "scripts": { 25 | "clean": "rm -rf ./lib", 26 | "compile": "babel --verbose -d lib/ src/", 27 | "lint": "eslint ./src ./test", 28 | "test": "jest", 29 | "prepublish": "npm run clean && npm run test && npm run compile" 30 | }, 31 | "dependencies": { 32 | "prop-types": "^15.6.0", 33 | "uuid": "^3.4.0" 34 | }, 35 | "peerDependencies": { 36 | "react": "^16.8.0", 37 | "react-addons-create-fragment": "^15.6.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.8.3", 41 | "@babel/core": "^7.8.3", 42 | "@babel/plugin-proposal-class-properties": "^7.8.3", 43 | "@babel/preset-env": "^7.8.3", 44 | "@babel/preset-react": "^7.8.3", 45 | "babel-eslint": "^10.0.3", 46 | "babel-jest": "^25.1.0", 47 | "enzyme": "^3.11.0", 48 | "enzyme-adapter-react-16": "^1.15.2", 49 | "enzyme-to-json": "^3.4.3", 50 | "eslint": "^6.8.0", 51 | "eslint-config-prettier": "^6.9.0", 52 | "eslint-config-unobtrusive": "^1.2.5", 53 | "eslint-plugin-prettier": "^3.1.2", 54 | "eslint-plugin-react": "^7.18.0", 55 | "jest": "^25.1.0", 56 | "prettier": "^1.19.1", 57 | "react": "^16.12.0", 58 | "react-addons-create-fragment": "^15.6.2", 59 | "react-dom": "^16.12.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | trailingComma: "all", 4 | singleQuote: false, 5 | }; 6 | -------------------------------------------------------------------------------- /src/conduit-provider.js: -------------------------------------------------------------------------------- 1 | import { Component, Children } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Registry from "./registry"; 4 | 5 | class ConduitProvider extends Component { 6 | constructor(props) { 7 | super(props); 8 | const { registry } = this.props; 9 | 10 | this.registry = registry ? registry : new Registry(); 11 | } 12 | 13 | getChildContext() { 14 | return { registry: this.registry }; 15 | } 16 | 17 | render() { 18 | return Children.only(this.props.children); 19 | } 20 | } 21 | 22 | ConduitProvider.childContextTypes = { 23 | registry: PropTypes.object.isRequired, 24 | }; 25 | 26 | export default ConduitProvider; 27 | -------------------------------------------------------------------------------- /src/conduit.js: -------------------------------------------------------------------------------- 1 | class Conduit { 2 | constructor(inlet, outlet) { 3 | this.inlet = inlet; 4 | this.outlet = outlet; 5 | } 6 | 7 | disconnect = () => { 8 | this.inlet.onDisconnect(this); 9 | this.outlet.onDisconnect(this); 10 | }; 11 | 12 | connect = () => { 13 | this.inlet.onConnect(this); 14 | this.outlet.onConnect(this); 15 | }; 16 | 17 | getId = () => `${this.inlet.getId()}-${this.outlet.getId()}`; 18 | getInlet = () => this.inlet; 19 | getOutlet = () => this.outlet; 20 | update = () => this.outlet.forceRender(); 21 | members = () => ({ inlet: this.inlet, outlet: this.outlet }); 22 | toString = () => `${this.getId()}: ${this.inlet.getLabel()} -> ${this.outlet.getLabel()}`; 23 | } 24 | 25 | export default Conduit; 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as ConduitProvider } from "./conduit-provider"; 2 | export { default as Inlet } from "./inlet"; 3 | export { default as Outlet } from "./outlet"; 4 | export { default as Registry } from "./registry"; 5 | -------------------------------------------------------------------------------- /src/inlet.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import uuidV4 from "uuid/v4"; 4 | 5 | export class Inlet extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.id = uuidV4(); 9 | } 10 | 11 | componentDidUpdate(prevProps) { 12 | if (this.props.label !== prevProps.label) { 13 | this.context.registry.rewireInlet(this); 14 | } 15 | this.context.registry.updateConduits(this); 16 | } 17 | 18 | UNSAFE_componentWillMount() { 19 | this.context.registry.registerInlet(this); 20 | } 21 | 22 | componentWillUnmount() { 23 | this.context.registry.unregisterInlet(this); 24 | } 25 | 26 | // Registry Inlet API 27 | getId = () => this.id; 28 | getLabel = () => this.props.label; 29 | getIndex = () => this.props.index; 30 | getChildren = () => this.props.children; 31 | onDisconnect = conduit => { 32 | this.props.onDisconnect(conduit.members()); 33 | }; 34 | onConnect = conduit => { 35 | this.props.onConnect(conduit.members()); 36 | }; 37 | 38 | render() { 39 | return null; 40 | } 41 | } 42 | 43 | Inlet.contextTypes = { 44 | registry: PropTypes.object.isRequired, 45 | }; 46 | 47 | Inlet.propTypes = { 48 | label: PropTypes.string.isRequired, 49 | index: PropTypes.number, 50 | onDisconnect: PropTypes.func, 51 | onConnect: PropTypes.func, 52 | }; 53 | 54 | Inlet.defaultProps = { 55 | index: 0, 56 | onDisconnect: () => {}, 57 | onConnect: () => {}, 58 | }; 59 | 60 | export default Inlet; 61 | -------------------------------------------------------------------------------- /src/outlet.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import uuidV4 from "uuid/v4"; 4 | 5 | class Outlet extends Component { 6 | constructor(props, context) { 7 | super(props, context); 8 | 9 | this.id = uuidV4(); 10 | this.state = { children: this.getChildren() }; 11 | } 12 | 13 | componentDidUpdate(prevProps) { 14 | if (this.props.label !== prevProps.label) { 15 | this.context.registry.rewireOutlet(this); 16 | } 17 | } 18 | 19 | componentDidMount() { 20 | this.context.registry.registerOutlet(this); 21 | } 22 | 23 | componentWillUnmount() { 24 | this.context.registry.unregisterOutlet(this); 25 | } 26 | 27 | getChildren = () => this.context.registry.mergeChildrenForLabel(this.getLabel()); 28 | 29 | // Registry Outlet API 30 | getId = () => this.id; 31 | getLabel = () => this.props.label; 32 | onDisconnect = conduit => { 33 | this.props.onDisconnect(conduit.members()); 34 | }; 35 | onConnect = conduit => { 36 | this.props.onConnect(conduit.members()); 37 | }; 38 | forceRender = () => this.setState({ children: this.getChildren() }); 39 | 40 | render() { 41 | return ( 42 |
43 | {this.state.children} 44 |
45 | ); 46 | } 47 | } 48 | 49 | Outlet.contextTypes = { 50 | registry: PropTypes.object.isRequired, 51 | }; 52 | 53 | Outlet.propTypes = { 54 | label: PropTypes.string.isRequired, 55 | className: PropTypes.string, 56 | style: PropTypes.object, 57 | onDisconnect: PropTypes.func, 58 | onConnect: PropTypes.func, 59 | }; 60 | 61 | Outlet.defaultProps = { 62 | className: null, 63 | style: null, 64 | onDisconnect: () => {}, 65 | onConnect: () => {}, 66 | }; 67 | 68 | export default Outlet; 69 | -------------------------------------------------------------------------------- /src/poor-man-ramda.js: -------------------------------------------------------------------------------- 1 | /* global Reflect */ 2 | export const values = obj => Object.values(obj); 3 | 4 | export const assoc = (k, v, obj) => ({ ...obj, [k]: v }); 5 | 6 | export const dissoc = (k, obj) => { 7 | const ret = { ...obj }; 8 | Reflect.deleteProperty(ret, k); 9 | return ret; 10 | }; 11 | -------------------------------------------------------------------------------- /src/registry.js: -------------------------------------------------------------------------------- 1 | import createFragment from "react-addons-create-fragment"; 2 | import Conduit from "./conduit"; 3 | import { assoc, dissoc, values } from "./poor-man-ramda"; 4 | 5 | class Registry { 6 | state = { 7 | outlets: {}, 8 | inlets: {}, 9 | conduits: {}, 10 | }; 11 | 12 | // Helper functions 13 | 14 | // find an inlet by id 15 | findInlet = id => this.state.inlets[id] || null; 16 | 17 | // find all inlets by label 18 | findInletsByLabel = label => values(this.state.inlets).filter(inlet => inlet.getLabel() === label); 19 | 20 | // add an inlet to the registry state 21 | // does not check current state 22 | assocInlet = inlet => { 23 | this.state.inlets = assoc(inlet.getId(), inlet, this.state.inlets); 24 | }; 25 | 26 | // remove an inlet from the registry state 27 | // does not check current state 28 | dissocInlet = inlet => { 29 | this.state.inlets = dissoc(inlet.getId(), this.state.inlets); 30 | }; 31 | 32 | // add conduits from the inlet to all matching outlets 33 | wireInlet = inlet => 34 | this.findOutletsByLabel(inlet.getLabel()).forEach(outlet => { 35 | this.registerConduit(new Conduit(inlet, outlet)); 36 | outlet.forceRender(); 37 | }); 38 | 39 | // disconnect all conduits from the inlet to all matching outlets. 40 | // does not remove the inlet. 41 | unwireInlet = inlet => { 42 | this.findConduitsByInlet(inlet).forEach(conduit => { 43 | this.unregisterConduit(conduit); 44 | conduit.getOutlet().forceRender(); 45 | }); 46 | }; 47 | 48 | // find an outlet by id 49 | findOutlet = id => this.state.outlets[id] || null; 50 | 51 | // find all outlets by label 52 | findOutletsByLabel = label => values(this.state.outlets).filter(outlet => outlet.getLabel() === label); 53 | 54 | // add an outlet to the registry state 55 | // does not check current state 56 | assocOutlet = outlet => { 57 | this.state.outlets = assoc(outlet.getId(), outlet, this.state.outlets); 58 | }; 59 | 60 | // remove an outlet from the registry state 61 | // does not check current state 62 | dissocOutlet = outlet => { 63 | this.state.outlets = dissoc(outlet.getId(), this.state.outlets); 64 | }; 65 | 66 | // add conduits from the outlet to all matching inlets 67 | wireOutlet = outlet => { 68 | this.findInletsByLabel(outlet.getLabel()).forEach(inlet => this.registerConduit(new Conduit(inlet, outlet))); 69 | outlet.forceRender(); 70 | }; 71 | 72 | // disconnect all conduits from the outlet to all matching inlets 73 | // does not remove the outlet 74 | unwireOutlet = outlet => { 75 | this.findConduitsByOutlet(outlet).forEach(this.unregisterConduit); 76 | outlet.forceRender(); 77 | }; 78 | 79 | // add a conduit to the registry state 80 | // does not check current state 81 | assocConduit = conduit => { 82 | this.state.conduits = assoc(conduit.getId(), conduit, this.state.conduits); 83 | }; 84 | 85 | // remove a conduit from the registry state 86 | // does not check current state 87 | dissocConduit = conduit => { 88 | this.state.conduits = dissoc(conduit.getId(), this.state.conduits); 89 | }; 90 | 91 | // register a conduit, invoking the corresponding callbacks and adding the conduit from the registry state 92 | registerConduit = conduit => { 93 | this.assocConduit(conduit); 94 | conduit.connect(); 95 | }; 96 | 97 | // unregister a conduit, invoking the corresponding callbacks and removing the conduit from the registry state 98 | unregisterConduit = conduit => { 99 | this.dissocConduit(conduit); 100 | conduit.disconnect(); 101 | }; 102 | 103 | // find a conduit by id 104 | findConduit = id => this.state.conduits[id] || null; 105 | 106 | // find all conduits for a given inlet 107 | findConduitsByInlet = inlet => 108 | values(this.state.conduits).filter(conduit => conduit.getInlet().getId() === inlet.getId()); 109 | 110 | // find all conduits for a given outlet 111 | findConduitsByOutlet = outlet => 112 | values(this.state.conduits).filter(conduit => conduit.getOutlet().getId() === outlet.getId()); 113 | 114 | // 115 | // Registry API 116 | // 117 | registerInlet = inlet => { 118 | // Add a new Inlet only if it is not registered 119 | if (this.findInlet(inlet.getId())) { 120 | return; 121 | } 122 | 123 | // Add the inlet to the inlet map and a new Conduit for every outlet with the same label 124 | this.assocInlet(inlet); 125 | this.wireInlet(inlet); 126 | }; 127 | 128 | unregisterInlet = inlet => { 129 | // Do nothing if the Outlet is not registered 130 | if (!this.findInlet(inlet.getId())) { 131 | return; 132 | } 133 | 134 | this.dissocInlet(inlet); 135 | this.unwireInlet(inlet); 136 | }; 137 | 138 | // full inlet rewire 139 | // useful when labels change 140 | rewireInlet = inlet => { 141 | this.unregisterInlet(inlet); 142 | this.registerInlet(inlet); 143 | }; 144 | 145 | registerOutlet = outlet => { 146 | // Do not add a new Outlet if it is already registered 147 | if (this.findOutlet(outlet.getId())) { 148 | return; 149 | } 150 | // Add the outlet to the outlet map and a new conduit for every inlet with the same label 151 | this.assocOutlet(outlet); 152 | this.wireOutlet(outlet); 153 | }; 154 | 155 | unregisterOutlet = outlet => { 156 | // Do nothing if the Outlet is not registered 157 | if (!this.findOutlet(outlet.getId())) { 158 | return; 159 | } 160 | 161 | this.dissocOutlet(outlet); 162 | this.unwireOutlet(outlet); 163 | }; 164 | 165 | // full outlet rewire 166 | // useful when labels change 167 | rewireOutlet = outlet => { 168 | this.unregisterOutlet(outlet); 169 | this.registerOutlet(outlet); 170 | }; 171 | 172 | // force a render for all outlets connected with a given inlet 173 | updateConduits = inlet => this.findConduitsByInlet(inlet).forEach(conduit => conduit.update()); 174 | 175 | // build children using all inlets for the given label 176 | mergeChildrenForLabel = label => 177 | createFragment( 178 | this.findInletsByLabel(label) 179 | .sort((i1, i2) => i1.props.index - i2.props.index) 180 | .map(inlet => [inlet.getId(), inlet.getChildren()]) 181 | .reduce((acc, [id, children]) => ({ ...acc, [id]: children }), {}), 182 | ); 183 | 184 | printConduits = () => values(this.state.conduits).forEach(c => console.log(c.toString())); 185 | } 186 | 187 | export default Registry; 188 | -------------------------------------------------------------------------------- /test/__snapshots__/registry.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`registry should merge all the children from matching inlets 1`] = ` 4 |
5 |
6 |
7 | c0 8 |
9 |
10 | c1 11 |
12 |
13 | c2 14 |
15 |
16 |
17 | `; 18 | -------------------------------------------------------------------------------- /test/__snapshots__/snapshots.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`conduit snapshots Inlet Inlet should not render its children 1`] = `null`; 4 | 5 | exports[`conduit snapshots Inlet and Outlet Inlet children are sent to their corresponding outlets 1`] = ` 6 |
7 |
10 |
11 |
12 | first inlet 13 |
14 |
15 | second inlet 16 |
17 |
18 |
19 |
22 |
23 |
24 | third inlet 25 |
26 |
27 |
28 |
29 | `; 30 | 31 | exports[`conduit snapshots Inlet and Outlet honors the inlet index props 1`] = ` 32 |
33 |
34 |
35 | first 36 |
37 |
38 | second 39 |
40 |
41 | last 42 |
43 |
44 |
45 | `; 46 | 47 | exports[`conduit snapshots Inlet and Outlet should merge the associated Inlet's children to the corresponding outlet 1`] = ` 48 |
49 |
50 |
51 | first inlet 52 |
53 |
54 | second inlet 55 |
56 |
57 | third inlet 58 |
59 |
60 |
61 | `; 62 | 63 | exports[`conduit snapshots Inlet and Outlet should output Inlet's children to the corresponding outlet 1`] = ` 64 |
65 |
66 | this should be rendered in the outlet 67 |
68 |
69 | `; 70 | 71 | exports[`conduit snapshots Inlet and Outlet the className and style of an outlet can be customized 1`] = ` 72 |
73 |
77 |
78 | inlet 79 |
80 |
81 |
82 | `; 83 | 84 | exports[`conduit snapshots Inlet and Outlet two outlets with the same label will duplicate the children rendering 1`] = ` 85 |
86 |
87 |
88 | inlet 89 |
90 |
91 |
92 |
93 | inlet 94 |
95 |
96 |
97 | `; 98 | 99 | exports[`conduit snapshots Outlet Outlet should not render its children 1`] = `
`; 100 | -------------------------------------------------------------------------------- /test/enzyme-setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslintConfig": { 3 | "globals": { 4 | "beforeEach": true, 5 | "describe": true, 6 | "expect": true, 7 | "it": true, 8 | "jest": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/poor-man-ramda.test.js: -------------------------------------------------------------------------------- 1 | import { values, assoc, dissoc } from "../src/poor-man-ramda"; 2 | 3 | describe("values", () => { 4 | it("returns the object values", () => { 5 | expect(values({})).toEqual([]); 6 | expect(values({ a: 1, b: 2, c: 3 })).toEqual([1, 2, 3]); 7 | }); 8 | }); 9 | 10 | describe("assoc", () => { 11 | it("adds a key to an object", () => { 12 | const o = { a: 1, b: 2 }; 13 | const a1 = assoc("b", 3, o); 14 | const a2 = assoc("c", 3, o); 15 | 16 | expect(a1).not.toBe(o); 17 | expect(a2).not.toBe(o); 18 | 19 | expect(o).toEqual({ a: 1, b: 2 }); 20 | expect(a1).toEqual({ a: 1, b: 3 }); 21 | expect(a2).toEqual({ a: 1, b: 2, c: 3 }); 22 | }); 23 | }); 24 | 25 | describe("dissoc", () => { 26 | it("removes a key from an object", () => { 27 | const o = { a: 1, b: 2, c: 3 }; 28 | const d1 = dissoc("a", o); 29 | const d2 = dissoc("d", o); 30 | 31 | expect(d1).not.toBe(o); 32 | expect(d2).not.toBe(o); 33 | 34 | expect(o).toEqual({ a: 1, b: 2, c: 3 }); 35 | expect(d1).toEqual({ b: 2, c: 3 }); 36 | expect(d2).toEqual(o); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/registry.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount, render } from "enzyme"; 3 | import { ConduitProvider, Inlet, Outlet, Registry } from "../src"; 4 | 5 | const mountWithRegister = (element, registry) => mount(element, { context: { registry } }); 6 | 7 | describe("registry", () => { 8 | let registry; 9 | 10 | beforeEach(() => { 11 | registry = new Registry(); 12 | }); 13 | 14 | it("should merge all the children from matching inlets", () => { 15 | const wrapper = render( 16 | 17 |
18 | 19 |
c1
20 |
21 | 22 |
c2
23 |
24 | 25 |
c0
26 |
27 | 28 |
29 |
, 30 | ); 31 | 32 | expect(wrapper).toMatchSnapshot(); 33 | }); 34 | 35 | it("should update outlet when inlets change their children", () => { 36 | const inlet = mountWithRegister( 37 | 38 |

content

39 |
, 40 | registry, 41 | ); 42 | const outlet = mountWithRegister(, registry); 43 | 44 | expect(outlet.html()).toEqual("

content

"); 45 | inlet.setProps({ children:

new content

}); 46 | expect(outlet.html()).toEqual("

new content

"); 47 | }); 48 | 49 | it("should update outlets when inlets change their labels", () => { 50 | const inlet = mountWithRegister( 51 | 52 |

content

53 |
, 54 | registry, 55 | ); 56 | const outlet1 = mountWithRegister(, registry); 57 | const outlet2 = mountWithRegister(, registry); 58 | 59 | expect(inlet.html()).toBe(null); 60 | expect(outlet1.html()).toEqual("

content

"); 61 | expect(outlet2.html()).toEqual("
"); 62 | inlet.setProps({ label: "2" }); 63 | expect(outlet1.html()).toEqual("
"); 64 | expect(outlet2.html()).toEqual("

content

"); 65 | }); 66 | 67 | it("should update outlet when it changes its label", () => { 68 | const inlet1 = mountWithRegister( 69 | 70 |

content 1

71 |
, 72 | registry, 73 | ); 74 | const inlet2 = mountWithRegister( 75 | 76 |

content 2

77 |
, 78 | registry, 79 | ); 80 | const outlet = mountWithRegister(, registry); 81 | 82 | expect(inlet1.html()).toBe(null); 83 | expect(inlet2.html()).toBe(null); 84 | expect(outlet.html()).toEqual("

content 1

"); 85 | outlet.setProps({ label: "2" }); 86 | expect(outlet.html()).toEqual("

content 2

"); 87 | }); 88 | 89 | it("should update outlet when a new inlet is rendered", () => { 90 | const inlet1 = mountWithRegister( 91 | 92 |

content 1

93 |
, 94 | registry, 95 | ); 96 | const outlet = mountWithRegister(, registry); 97 | 98 | expect(inlet1.html()).toBe(null); 99 | expect(outlet.html()).toEqual("

content 1

"); 100 | mountWithRegister( 101 | 102 |

content 2

103 |
, 104 | registry, 105 | ); 106 | expect(outlet.html()).toEqual("

content 1

content 2

"); 107 | }); 108 | 109 | it("should update outlet when an inlet is unmounted", () => { 110 | const inlet1 = mountWithRegister( 111 | 112 |

content 1

113 |
, 114 | registry, 115 | ); 116 | const inlet2 = mountWithRegister( 117 | 118 |

content 2

119 |
, 120 | registry, 121 | ); 122 | const outlet = mountWithRegister(, registry); 123 | 124 | expect(inlet1.html()).toBe(null); 125 | expect(inlet2.html()).toBe(null); 126 | expect(outlet.html()).toEqual("

content 1

content 2

"); 127 | inlet2.unmount(); 128 | expect(outlet.html()).toEqual("

content 1

"); 129 | }); 130 | 131 | it("should invoke onDisconnect on both inlet and outlet when a conduit gets disconnected", () => { 132 | const inletSpy = jest.fn(); 133 | const outletSpy = jest.fn(); 134 | 135 | const inlet = mountWithRegister( 136 | 137 |

content

138 |
, 139 | registry, 140 | ); 141 | const outlet = mountWithRegister(, registry); 142 | 143 | expect(outlet.html()).toEqual("

content

"); 144 | inlet.unmount(); 145 | expect(inletSpy).toHaveBeenCalledTimes(1); 146 | expect(outletSpy).toHaveBeenCalledTimes(1); 147 | }); 148 | 149 | it("should invoke onConnect on both inlet and outlet when a new conduit is connected", () => { 150 | const inletSpy = jest.fn(); 151 | const outletSpy = jest.fn(); 152 | 153 | const inlet = mountWithRegister( 154 | 155 |

content

156 |
, 157 | registry, 158 | ); 159 | const outlet = mountWithRegister(, registry); 160 | 161 | expect(inlet.html()).toBe(null); 162 | expect(outlet.html()).toEqual("

content

"); 163 | expect(inletSpy).toHaveBeenCalledTimes(1); 164 | expect(outletSpy).toHaveBeenCalledTimes(1); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/snapshots.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "enzyme"; 3 | import { ConduitProvider, Inlet, Outlet } from "../src"; 4 | 5 | const renderWithProvider = element => render({element}); 6 | 7 | describe("conduit snapshots", () => { 8 | describe("Inlet", () => { 9 | it("Inlet should not render its children", () => { 10 | const wrapper = renderWithProvider( 11 | 12 |
this shouldn't be seen
13 |
, 14 | ); 15 | expect(wrapper).toMatchSnapshot(); 16 | }); 17 | }); 18 | 19 | describe("Outlet", () => { 20 | it("Outlet should not render its children", () => { 21 | const wrapper = renderWithProvider( 22 | 23 |
this shouldn't be seen
24 |
, 25 | ); 26 | expect(wrapper).toMatchSnapshot(); 27 | }); 28 | }); 29 | 30 | describe("Inlet and Outlet", () => { 31 | it("should output Inlet's children to the corresponding outlet", () => { 32 | const wrapper = renderWithProvider( 33 |
34 | this should be rendered in the outlet 35 | 36 | 37 |
, 38 | ); 39 | expect(wrapper).toMatchSnapshot(); 40 | }); 41 | 42 | it("should merge the associated Inlet's children to the corresponding outlet", () => { 43 | const wrapper = renderWithProvider( 44 |
45 | 46 |
first inlet
47 |
48 | 49 |
second inlet
50 |
51 | 52 |
third inlet
53 |
54 | 55 | 56 |
, 57 | ); 58 | expect(wrapper).toMatchSnapshot(); 59 | }); 60 | 61 | it("Inlet children are sent to their corresponding outlets", () => { 62 | const wrapper = renderWithProvider( 63 |
64 | 65 |
first inlet
66 |
67 | 68 |
second inlet
69 |
70 | 71 |
third inlet
72 |
73 | 74 |
75 | 76 |
77 |
78 | 79 |
80 |
, 81 | ); 82 | expect(wrapper).toMatchSnapshot(); 83 | }); 84 | 85 | it("two outlets with the same label will duplicate the children rendering", () => { 86 | const wrapper = renderWithProvider( 87 |
88 | 89 |
inlet
90 |
91 | 92 | 93 | 94 |
, 95 | ); 96 | expect(wrapper).toMatchSnapshot(); 97 | }); 98 | 99 | it("the className and style of an outlet can be customized", () => { 100 | const wrapper = renderWithProvider( 101 |
102 | 103 |
inlet
104 |
105 | 106 |
, 107 | ); 108 | expect(wrapper).toMatchSnapshot(); 109 | }); 110 | 111 | it("honors the inlet index props", () => { 112 | const wrapper = renderWithProvider( 113 |
114 | 115 |
last
116 |
117 | 118 |
second
119 |
120 | 121 |
first
122 |
123 | 124 |
, 125 | ); 126 | expect(wrapper).toMatchSnapshot(); 127 | }); 128 | }); 129 | }); 130 | --------------------------------------------------------------------------------