├── .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 |
65 |
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 |
Remove Items
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 |
81 | );
82 | });
83 |
84 | it('should render multiple children into a single GatewayDest', function() {
85 | assertEqual(
86 |
87 |
88 |
89 |
90 | One
91 |
92 |
97 |
98 | Three
99 |
100 |
101 |
102 |
103 | ,
104 | // should equal
105 |
106 |
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 |
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 |
--------------------------------------------------------------------------------