├── .babelrc ├── .eslintrc ├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example ├── .gitignore ├── Application.js ├── browser.js ├── server.js └── styles.css ├── karma.conf.js ├── package.json ├── src ├── Gateway.js ├── GatewayDest.js ├── GatewayProvider.js ├── GatewayRegistry.js └── index.js └── test ├── .eslintrc └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["cf"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # vim: set ft=yaml: 2 | --- 3 | parser: "babel-eslint" 4 | env: 5 | browser: true 6 | node: true 7 | plugins: 8 | - "cflint" 9 | rules: 10 | # Possible Errors 11 | quotes: 12 | - 2 13 | - "single" 14 | # Best Practices 15 | block-scoped-var: 2 16 | default-case: 2 17 | guard-for-in: 2 18 | no-else-return: 2 19 | no-floating-decimal: 2 20 | no-self-compare: 2 21 | no-void: 2 22 | radix: 2 23 | ## Possible that this will switch to a "2" later 24 | wrap-iife: 2 25 | # Strict Mode 26 | strict: 27 | - 1 28 | - "never" 29 | # Variables 30 | no-catch-shadow: 2 31 | # Node.js 32 | handle-callback-err: 2 33 | # Stylistic Issues 34 | brace-style: 35 | - 1 36 | - "1tbs" 37 | camelcase: 0 38 | comma-style: 39 | - 2 40 | - "last" 41 | no-lonely-if: 2 42 | no-nested-ternary: 2 43 | no-underscore-dangle: 0 44 | quote-props: 45 | - 1 46 | - "as-needed" 47 | semi: 48 | - 2 49 | - "always" 50 | space-unary-ops: 2 51 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | schedule: 9 | - cron: '0 0 * * *' 10 | name: Semgrep config 11 | jobs: 12 | semgrep: 13 | name: semgrep/ci 14 | runs-on: ubuntu-latest 15 | env: 16 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 17 | SEMGREP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 20 | container: 21 | image: semgrep/semgrep 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | src 3 | coverage 4 | test 5 | example 6 | karma.conf.js 7 | .babelrc 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, CloudFlare, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Gateway 2 | 3 | > Render React DOM into a new context (aka "Portal") 4 | 5 | This can be used to implement various UI components such as modals. 6 | See [`react-modal2`](https://github.com/cloudflare/react-modal2). 7 | 8 | It also works in universal (isomorphic) React applications without any 9 | additional setup and in React Native applications 10 | [when used correctly](#react-native-example). 11 | 12 | ## Installation 13 | 14 | ```sh 15 | $ npm install --save react-gateway 16 | ``` 17 | 18 | ## Example 19 | 20 | ```js 21 | import React from 'react'; 22 | import { 23 | Gateway, 24 | GatewayDest, 25 | GatewayProvider 26 | } from 'react-gateway'; 27 | 28 | export default class Application extends React.Component { 29 | render() { 30 | return ( 31 | 32 |
33 |

React Gateway Universal Example

34 |
35 | 36 |
Item 1
37 |
38 | 39 |
Item 2
40 |
41 |
Item 3
42 |
43 | 44 | 45 |
46 |
47 | ); 48 | } 49 | } 50 | ``` 51 | 52 | Will render as: 53 | 54 | ```js 55 |
56 |

React Gateway Universal Example

57 |
58 | 59 | 60 |
Item 3
61 |
62 |
63 |
Item 1
64 |
65 |
66 |
Item 2
67 |
68 |
69 | ``` 70 | 71 | ## Usage 72 | 73 | To get started with React Gateway, first wrap your application in the 74 | ``. 75 | 76 | ```diff 77 | import React from 'react'; 78 | + import { 79 | + GatewayProvider 80 | + } from 'react-gateway'; 81 | 82 | export default class Application extends React.Component { 83 | render() { 84 | return ( 85 | + 86 |
87 | {this.props.children} 88 |
89 | +
90 | ); 91 | } 92 | } 93 | ``` 94 | 95 | Then insert a `` whereever you want it to render and give it a 96 | name. 97 | 98 | ```diff 99 | import React from 'react'; 100 | import { 101 | GatewayProvider, 102 | + GatewayDest 103 | } from 'react-gateway'; 104 | 105 | export default class Application extends React.Component { 106 | render() { 107 | return ( 108 | 109 |
110 | {this.props.children} 111 | + 112 |
113 |
114 | ); 115 | } 116 | } 117 | ``` 118 | 119 | Then in any of your components (that get rendered inside of the 120 | ``) add a ``. 121 | 122 | ```diff 123 | import React from 'react'; 124 | + import {Gateway} from 'react-gateway'; 125 | 126 | export default class MyComponent extends React.Component { 127 | render() { 128 | return ( 129 |
130 | + 131 | + Will render into the "global" gateway. 132 | + 133 |
134 | ); 135 | } 136 | } 137 | ``` 138 | 139 | If you want to customize the `` element, you can pass any props, 140 | including `component` (which will allows you to specify a `tagName` or custom 141 | component), and they will be passed to the created element. 142 | 143 | ```diff 144 | export default class Application extends React.Component { 145 | render() { 146 | return ( 147 | 148 |
149 | {this.props.children} 150 | - 151 | + 152 |
153 |
154 | ); 155 | } 156 | } 157 | ``` 158 | 159 | ## How it works 160 | 161 | React Gateway works very differently than most React "portals" in order to work 162 | in server-side rendered React applications. 163 | 164 | It maintains an internal registry of "containers" and "children" which manages 165 | where things should be rendered. 166 | 167 | This registry is created by `` and passed to `` and 168 | `` invisibly via React's `contextTypes`. 169 | 170 | Whenever a child or container is added or removed, React Gateway will 171 | update its internal registry and ensure things are properly rendered. 172 | 173 | ## React Native example 174 | 175 | React Gateway does not directly depend on `react-dom`, so it works fine with 176 | React Native under one condition: 177 | 178 | **You must pass React Native component like `View` or similar to 179 | `component` prop of ``.** 180 | 181 | Because if you don't, `` will try to render `div` element, which 182 | is not available. 183 | 184 | ```js 185 | import React from 'react'; 186 | import { Text, View } from 'react-native'; 187 | import { 188 | Gateway, 189 | GatewayDest, 190 | GatewayProvider 191 | } from 'react-gateway'; 192 | 193 | export default class Application extends React.Component { 194 | render() { 195 | return ( 196 | 197 | 198 | React Gateway Native Example 199 | 200 | 201 | Text rendered elsewhere 202 | 203 | 204 | 205 | 206 | 207 | ); 208 | } 209 | } 210 | ``` 211 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | -------------------------------------------------------------------------------- /example/Application.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Gateway, 4 | GatewayDest, 5 | GatewayProvider 6 | } from '../src/index'; 7 | 8 | class Application extends React.Component { 9 | state = { 10 | open: true 11 | }; 12 | 13 | handleClick() { 14 | this.setState({ 15 | open: false 16 | }); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |

React Gateway Universal Example

23 | 24 | {this.state.open && ( 25 |
26 | 27 |
Item 1
28 |
29 | 30 |
Item 2
31 |
32 |
Item 3
33 |
34 | )} 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | 42 | export default class Root extends React.Component { 43 | render() { 44 | return ( 45 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Application from './Application'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import React from 'react'; 4 | import ReactDOMServer from 'react-dom/server'; 5 | 6 | import Application from './Application'; 7 | 8 | const app = express(); 9 | 10 | app.get('/', (req, res) => { 11 | res.send(` 12 | 13 | 14 | 15 | React Gateway Universal Example 16 | 17 | 18 | 19 |
${ReactDOMServer.renderToString()}
20 | 21 | 22 | 23 | `); 24 | }); 25 | 26 | app.get('/bundle.js', (req, res) => res.sendFile(path.join(__dirname, 'bundle.js'))); 27 | app.get('/styles.css', (req, res) => res.sendFile(path.join(__dirname, 'styles.css'))); 28 | 29 | app.listen(3000); 30 | console.log('>> http://localhost:3000/'); 31 | -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 1em/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | } 4 | 5 | .container { 6 | background: #eee; 7 | margin: 0.5em 0; 8 | padding: 0.5em 1em; 9 | border: 2px solid #ddd; 10 | } 11 | 12 | .item { 13 | background: #ddd; 14 | padding: 1em; 15 | margin: 0.5em 0; 16 | border: 2px solid #ccc; 17 | } 18 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var customLaunchers = {}; 3 | 4 | var minimist = require('minimist'); 5 | var defined = require('defined'); 6 | 7 | var args = minimist(process.argv.slice(2), { 8 | string: ['env', 'build-branch', 'build-number', 'suace-username', 'sauce-key'], 9 | 'default': { 10 | env: process.env.NODE_ENV, 11 | 'build-branch': process.env.BUILD_BRANCH, 12 | 'build-number': process.env.BUILD_NUMBER, 13 | 'sauce-username': process.env.SAUCE_USERNAME, 14 | 'sauce-key': process.env.SAUCE_ACCESS_KEY 15 | } 16 | }); 17 | 18 | args.istanbul = defined(args.istanbul, args.env !== 'CI'); 19 | args['sauce-labs'] = defined(args['sauce-labs'], args.env === 'CI'); 20 | 21 | // Overridable arguments are denoted below. Other arguments can be found in the 22 | // [Karma configuration](http://karma-runner.github.io/0.12/config/configuration-file.html) 23 | 24 | ['chrome', 'firefox', 'iphone', 'ipad', 'android'].forEach(function(browser) { 25 | customLaunchers['sl_' + browser] = { 26 | base: 'SauceLabs', 27 | browserName: browser 28 | }; 29 | }); 30 | 31 | // Safari defaults to version 5 on Windows 7 (huh?) 32 | customLaunchers.sl_safari = { 33 | base: 'SauceLabs', 34 | browserName: 'safari', 35 | platform: 'OS X 10.9' 36 | }; 37 | 38 | [9, 10, 11].forEach(function(version) { 39 | customLaunchers['sl_ie_' + version] = { 40 | base: 'SauceLabs', 41 | browserName: 'internet explorer', 42 | version: version 43 | }; 44 | }); 45 | 46 | var reporters = ['mocha', 'beep']; 47 | 48 | if (args.istanbul) { 49 | reporters.push('coverage'); 50 | } 51 | 52 | module.exports = function(config) { 53 | config.set({ 54 | frameworks: ['browserify', 'mocha'], 55 | 56 | files: [ 57 | { 58 | pattern: 'http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min.js', 59 | watched: false, 60 | included: true, 61 | served: false 62 | }, 63 | { 64 | pattern: 'test/**/*.js', 65 | watched: true, 66 | included: true, 67 | served: true 68 | } 69 | ], 70 | 71 | preprocessors: { 72 | 'test/**/*.js': ['browserify'] 73 | }, 74 | 75 | // Overridable with a comma-separated list with `--reporters` 76 | reporters: reporters, 77 | 78 | // Overridable with `[--no]-colors 79 | colors: true, 80 | 81 | /* Overridable with `--log-level=`. 82 | * 83 | * Possible 'level' options include: 84 | * * disable 85 | * * error 86 | * * warn 87 | * * info 88 | * * debug 89 | */ 90 | logLevel: config.LOG_INFO, 91 | 92 | // Overridable with a comma-separated list with `--browsers` 93 | browsers: args['sauce-labs'] ? Object.keys(customLaunchers) : [ 94 | 'Chrome', 95 | 'Firefox', 96 | 'Safari' 97 | ], 98 | 99 | sauceLabs: { 100 | username: args['sauce-username'], 101 | accessKey: args['sauce-key'], 102 | testName: require('./package.json').name, 103 | tags: args['build-branch'], 104 | build: args['build-number'] 105 | }, 106 | 107 | browserify: { 108 | debug: true, 109 | transform: [ 110 | 'babelify' 111 | ].concat(args.istanbul && [ 112 | ['browserify-istanbul', { 113 | ignore: ['**/*.handlebars'] 114 | }] 115 | ] || []) 116 | }, 117 | 118 | coverageReporter: { 119 | reporters: [ 120 | { 121 | type: 'html' 122 | }, 123 | { 124 | type: 'text' 125 | } 126 | ] 127 | }, 128 | 129 | client: { 130 | // '--grep' arguments are passed directly to mocha. 131 | args: args.grep && ['--grep', args.grep], 132 | mocha: { 133 | reporter: 'html' 134 | } 135 | }, 136 | 137 | customLaunchers: customLaunchers 138 | }); 139 | }; 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-gateway", 3 | "version": "3.0.0", 4 | "description": "Render React DOM into a new context", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src -d lib", 8 | "example": "browserify example/browser.js -o example/bundle.js -t babelify --debug && babel-node example/server.js", 9 | "format": "jsfmt -w *.js src/ test/", 10 | "lint": "eslint src/ test/", 11 | "prepublish": "npm run build", 12 | "test": "karma start" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "gateway", 17 | "portal", 18 | "modal", 19 | "dialog" 20 | ], 21 | "author": "James Kyle ", 22 | "repository": "https://github.com/cloudflare/react-gateway", 23 | "license": "BSD-3-Clause", 24 | "dependencies": { 25 | "prop-types": "^15.5.0", 26 | "react-prop-types": "^0.4.0" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.1.1", 30 | "babel-core": "^6.0.20", 31 | "babel-eslint": "^6.0.2", 32 | "babel-preset-cf": "^1.2.0", 33 | "babelify": "^7.2.0", 34 | "browserify-istanbul": "^0.2.1", 35 | "chai": "^3.4.1", 36 | "defined": "^1.0.0", 37 | "eslint": "^1.8.0", 38 | "eslint-plugin-cflint": "^1.0.0", 39 | "express": "^4.13.3", 40 | "jsfmt": "^0.5.2", 41 | "karma": "^0.13.15", 42 | "karma-beep-reporter": "^0.1.4", 43 | "karma-browserify": "^4.4.0", 44 | "karma-chrome-launcher": "^0.2.1", 45 | "karma-coverage": "^0.5.3", 46 | "karma-firefox-launcher": "^0.1.6", 47 | "karma-mocha": "^0.2.0", 48 | "karma-mocha-reporter": "^1.1.1", 49 | "karma-safari-launcher": "^0.1.1", 50 | "karma-sauce-launcher": "^0.3.0", 51 | "karma-tape-reporter": "^1.0.3", 52 | "minimist": "^1.2.0", 53 | "mocha": "^2.3.3", 54 | "react-addons-test-utils": "^15.0.0-0", 55 | "react": "^15.0.0-0", 56 | "react-dom": "^15.0.0-0" 57 | }, 58 | "peerDependencies": { 59 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Gateway.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import GatewayRegistry from './GatewayRegistry'; 4 | 5 | export default class Gateway extends React.Component { 6 | static contextTypes = { 7 | gatewayRegistry: PropTypes.instanceOf(GatewayRegistry).isRequired 8 | }; 9 | 10 | static propTypes = { 11 | into: PropTypes.string.isRequired, 12 | children: PropTypes.node 13 | }; 14 | 15 | constructor(props, context) { 16 | super(props, context); 17 | this.gatewayRegistry = context.gatewayRegistry; 18 | } 19 | 20 | componentWillMount() { 21 | this.id = this.gatewayRegistry.register( 22 | this.props.into, 23 | this.props.children 24 | ); 25 | this.renderIntoGatewayNode(this.props); 26 | } 27 | 28 | componentWillReceiveProps(props) { 29 | this.gatewayRegistry.clearChild(this.props.into, this.id); 30 | this.renderIntoGatewayNode(props); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.gatewayRegistry.unregister(this.props.into, this.id); 35 | } 36 | 37 | renderIntoGatewayNode(props) { 38 | this.gatewayRegistry.addChild(this.props.into, this.id, props.children); 39 | } 40 | 41 | render() { 42 | return null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/GatewayDest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import GatewayRegistry from './GatewayRegistry'; 4 | import {deprecated} from 'react-prop-types'; 5 | 6 | export default class GatewayDest extends React.Component { 7 | static contextTypes = { 8 | gatewayRegistry: PropTypes.instanceOf(GatewayRegistry).isRequired 9 | }; 10 | 11 | static propTypes = { 12 | name: PropTypes.string.isRequired, 13 | tagName: deprecated(PropTypes.string, 'Use "component" instead.'), 14 | component: PropTypes.oneOfType([ 15 | PropTypes.string, 16 | PropTypes.func 17 | ]) 18 | }; 19 | 20 | constructor(props, context) { 21 | super(props, context); 22 | this.gatewayRegistry = context.gatewayRegistry; 23 | } 24 | 25 | state = { 26 | children: null 27 | }; 28 | 29 | componentWillMount() { 30 | this.gatewayRegistry.addContainer(this.props.name, this); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.gatewayRegistry.removeContainer(this.props.name, this); 35 | } 36 | 37 | render() { 38 | const { component, tagName, ...attrs } = this.props; 39 | delete attrs.name; 40 | return React.createElement(component || tagName || 'div', attrs, this.state.children); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/GatewayProvider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import GatewayRegistry from './GatewayRegistry'; 4 | 5 | export default class GatewayProvider extends React.Component { 6 | static childContextTypes = { 7 | gatewayRegistry: PropTypes.instanceOf(GatewayRegistry).isRequired 8 | }; 9 | 10 | getChildContext() { 11 | return { 12 | gatewayRegistry: this.gatewayRegistry 13 | }; 14 | } 15 | 16 | static propTypes = { 17 | children: PropTypes.element, 18 | }; 19 | 20 | constructor(props, context) { 21 | super(props, context); 22 | this.gatewayRegistry = new GatewayRegistry(); 23 | } 24 | 25 | render() { 26 | return this.props.children; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GatewayRegistry.js: -------------------------------------------------------------------------------- 1 | export default class GatewayRegistry { 2 | constructor() { 3 | this._containers = {}; 4 | this._children = {}; 5 | 6 | // Unique key for children of a gateway 7 | this._currentId = 0; 8 | } 9 | 10 | _renderContainer(name) { 11 | if (!this._containers[name] || !this._children[name]) { 12 | return; 13 | } 14 | 15 | this._containers[name].setState({ 16 | children: Object.keys(this._children[name]).sort().map(id => this._children[name][id]) 17 | }); 18 | } 19 | 20 | addContainer(name, container) { 21 | this._containers[name] = container; 22 | this._renderContainer(name); 23 | } 24 | 25 | removeContainer(name) { 26 | this._containers[name] = null; 27 | } 28 | 29 | addChild(name, gatewayId, child) { 30 | this._children[name][gatewayId] = child; 31 | this._renderContainer(name); 32 | } 33 | 34 | clearChild(name, gatewayId) { 35 | delete this._children[name][gatewayId]; 36 | } 37 | 38 | register(name, child) { 39 | this._children[name] = this._children[name] || {}; 40 | 41 | const gatewayId = `${name}_${this._currentId}`; 42 | this._children[name][gatewayId] = child; 43 | this._currentId += 1; 44 | 45 | return gatewayId; 46 | } 47 | 48 | unregister(name, gatewayId) { 49 | this.clearChild(name, gatewayId); 50 | this._renderContainer(name); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export Gateway from './Gateway'; 2 | export GatewayDest from './GatewayDest'; 3 | export GatewayProvider from './GatewayProvider'; 4 | export GatewayRegistry from './GatewayRegistry'; 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | # vim: set ft=yaml: 2 | --- 3 | globals: 4 | describe: false 5 | it: false 6 | before: false 7 | after: false 8 | beforeEach: false 9 | afterEach: false 10 | rules: 11 | no-unused-expressions: 0 12 | new-cap: 0 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import ReactDOMServer from 'react-dom/server'; 6 | import { 7 | Gateway, 8 | GatewayDest, 9 | GatewayProvider, 10 | GatewayRegistry 11 | } from '../src/index.js'; 12 | 13 | function render(jsx) { 14 | return ReactDOMServer.renderToStaticMarkup(jsx); 15 | } 16 | 17 | function assertEqual(actual, expected) { 18 | expect(render(actual)).to.equal(render(expected)); 19 | } 20 | 21 | describe('Gateway', function() { 22 | it('should render Gateway in GatewayDest', function() { 23 | assertEqual( 24 | 25 |
26 |
27 | 28 | Hello World 29 | 30 |
31 | 32 |
33 |
, 34 | // should equal 35 |
36 |
37 |
Hello World
38 |
39 | ); 40 | }); 41 | 42 | it('should be able to customize the GatewayDest element', function() { 43 | assertEqual( 44 | 45 | 46 | , 47 | // should equal 48 |
49 | ); 50 | }); 51 | 52 | it('should be able to customize the GatewayDest with custom components', function() { 53 | function Child(props) { 54 | return

{props.children}

; 55 | } 56 | 57 | assertEqual( 58 | 59 | 60 | , 61 | // should equal 62 | 63 | ); 64 | }); 65 | 66 | it('should render into the correct GatewayDest', function() { 67 | assertEqual( 68 | 69 |
70 | One 71 | Two 72 | 73 | 74 |
75 |
, 76 | // should equal 77 |
78 |
Two
79 |
One
80 |
81 | ); 82 | }); 83 | 84 | it('should render multiple children into a single GatewayDest', function() { 85 | assertEqual( 86 | 87 |
88 |
89 | 90 |
One
91 |
92 |
93 | 94 |
Two
95 |
96 |
97 | 98 |
Three
99 |
100 |
101 | 102 |
103 |
, 104 | // should equal 105 |
106 |
107 |
108 |
109 |
110 |
One
111 |
Two
112 |
Three
113 |
114 |
115 | ); 116 | }); 117 | 118 | it('should pass context', function() { 119 | class Child extends React.Component { 120 | static contextTypes = { 121 | textContent: PropTypes.string.isRequired 122 | }; 123 | 124 | constructor(props, context) { 125 | super(props, context); 126 | this.textContent = context.textContent; 127 | } 128 | 129 | render() { 130 | return ( 131 | 132 | {this.textContent} 133 | 134 | ); 135 | } 136 | } 137 | 138 | class Parent extends React.Component { 139 | static childContextTypes = { 140 | textContent: PropTypes.string.isRequired 141 | }; 142 | 143 | getChildContext() { 144 | return { 145 | textContent: 'Hello from context' 146 | }; 147 | } 148 | 149 | render() { 150 | return ; 151 | } 152 | } 153 | 154 | class Application extends React.Component { 155 | render() { 156 | return ( 157 | 158 |
159 | 160 | 161 |
162 |
163 | ); 164 | } 165 | } 166 | 167 | assertEqual( 168 | , 169 | // should equal 170 |
171 |
172 | Hello from context 173 |
174 |
175 | ); 176 | }); 177 | }); 178 | 179 | describe('GatewayRegistry', function() { 180 | describe('register', function () { 181 | it('should return a gateway id', function () { 182 | const gatewayRegistry = new GatewayRegistry(); 183 | expect(gatewayRegistry.register('test', )).to.equal('test_0'); 184 | }); 185 | 186 | it('should increment intrernal ids', function () { 187 | const gatewayRegistry = new GatewayRegistry(); 188 | gatewayRegistry.register('test', ); 189 | gatewayRegistry.register('test', ); 190 | expect(gatewayRegistry._currentId).to.equal(2); 191 | }); 192 | }); 193 | }); 194 | --------------------------------------------------------------------------------