├── .eslintrc.json ├── .gitignore ├── README.md ├── lib ├── index.js └── portal │ ├── portal-context.js │ ├── portal-enter.js │ ├── portal-exit.js │ └── portal-provider.js ├── package.json ├── tests ├── __snapshots__ │ ├── example.test.js.snap │ ├── portal-enter.test.js.snap │ └── portal-exit.test.js.snap ├── example.test.js ├── portal-enter.test.js ├── portal-exit.test.js └── setup.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react", "prettier"], 4 | "extends": ["plugin:prettier/recommended"] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-gateway 2 | 3 | > Render React-Native component into a new context (aka "Portal") 4 | 5 | This can be used to implement various UI components such as modals. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | $ yarn add react-native-gateway 11 | ``` 12 | 13 | ## Example 14 | 15 | ```js 16 | import React from "react"; 17 | import { PortalEnter, PortalExit, PortalProvider } from "react-native-gateway"; 18 | 19 | const App = ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | ``` 30 | 31 | Will render as: 32 | 33 | ```js 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | ## Usage 45 | 46 | To get started with react-native-gateway, first wrap your application in the 47 | ``. 48 | 49 | ```diff 50 | import React from 'react'; 51 | + import { 52 | + PortalProvider 53 | + } from 'react-native-gateway'; 54 | 55 | const App = () => { 56 | return ( 57 | + 58 | 59 | {this.props.children} 60 | 61 | + 62 | ); 63 | } 64 | ``` 65 | 66 | Then insert a `` whereever you want it to render. 67 | 68 | ```diff 69 | import React from 'react'; 70 | import { 71 | PortalProvider, 72 | + PortalExit 73 | } from 'react-gateway'; 74 | 75 | const App = () => { 76 | return ( 77 | 78 | 79 | {this.props.children} 80 | + 81 | 82 | 83 | ); 84 | } 85 | ``` 86 | 87 | Then in any of your components (that get rendered inside of the 88 | ``) add a `` and add `name` prop to it. 89 | 90 | ```diff 91 | import React from 'react'; 92 | + import {PortalEnter} from 'react-native-gateway'; 93 | 94 | const App = () => { 95 | return ( 96 | 97 | + 98 | + Will render into the PortalExit. 99 | + 100 | 101 | ); 102 | } 103 | ``` 104 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export PortalContext from "./portal/portal-context"; 2 | export PortalEnter from "./portal/portal-enter"; 3 | export PortalExit from "./portal/portal-exit"; 4 | export PortalProvider from "./portal/portal-provider"; 5 | -------------------------------------------------------------------------------- /lib/portal/portal-context.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PortalContext = React.createContext({ 4 | elements: {}, 5 | addElement: () => {}, 6 | removeElement: () => {} 7 | }); 8 | 9 | export default PortalContext; 10 | -------------------------------------------------------------------------------- /lib/portal/portal-enter.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import PortalContext from "./portal-context"; 3 | 4 | const PortalEnter = ({ children, name }) => { 5 | const { addElement, removeElement } = useContext(PortalContext); 6 | 7 | useEffect(() => { 8 | removeElement({ name }); 9 | addElement({ 10 | name, 11 | component: children 12 | }); 13 | 14 | return () => removeElement({ name }); 15 | }, [children]); 16 | 17 | return null; 18 | }; 19 | 20 | export default PortalEnter; 21 | -------------------------------------------------------------------------------- /lib/portal/portal-exit.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import PortalContext from "./portal-context"; 3 | 4 | const PortalExit = () => { 5 | const { elements } = useContext(PortalContext); 6 | return Object.keys(elements).map(name => elements[name]); 7 | }; 8 | 9 | export default PortalExit; 10 | -------------------------------------------------------------------------------- /lib/portal/portal-provider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PortalContext from "./portal-context"; 3 | 4 | class PortalProvider extends Component { 5 | addElement = ({ name, component }) => { 6 | this.setState(state => ({ 7 | elements: { 8 | ...state.elements, 9 | [name]: component 10 | } 11 | })); 12 | }; 13 | 14 | removeElement = ({ name }) => { 15 | this.setState(state => { 16 | const { [name]: removedElement, ...restElements } = state.elements; 17 | return { 18 | elements: restElements 19 | }; 20 | }); 21 | }; 22 | 23 | state = { 24 | addElement: this.addElement, 25 | removeElement: this.removeElement, 26 | elements: {} 27 | }; 28 | 29 | render() { 30 | return ( 31 | 32 | {this.props.children} 33 | 34 | ); 35 | } 36 | } 37 | 38 | export default PortalProvider; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-gateway", 3 | "version": "1.0.0", 4 | "description": "Render React-Native component into new context", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "yarn lint && yarn unit", 8 | "unit": "yarn jest", 9 | "lint": "eslint lib/", 10 | "format": "eslint lib/ --fix" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/headfire94/react-native-gateway.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "portal", 19 | "gateway", 20 | "modal", 21 | "dialog", 22 | "react-native", 23 | "ios", 24 | "android" 25 | ], 26 | "author": "Egor Vodopyanov egorvodopyanov@gmail.com", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/headfire94/react-native-gateway/issues" 30 | }, 31 | "homepage": "https://github.com/headfire94/react-native-gateway#readme", 32 | "peerDependencies": { 33 | "react": "^16.8.1", 34 | "react-native": "^0.59.2" 35 | }, 36 | "devDependencies": { 37 | "babel-eslint": "^10.0.3", 38 | "babel-jest": "^24.9.0", 39 | "babel-preset-react-native": "^4.0.1", 40 | "enzyme": "^3.10.0", 41 | "enzyme-adapter-react-16": "^1.14.0", 42 | "enzyme-to-json": "^3.4.0", 43 | "eslint": "^6.2.2", 44 | "eslint-config-prettier": "^6.1.0", 45 | "eslint-plugin-prettier": "^3.1.0", 46 | "eslint-plugin-react": "^7.14.3", 47 | "jest": "^24.9.0", 48 | "jsdom": "^15.1.1", 49 | "prettier": "^1.18.2", 50 | "react": "^16.9.0", 51 | "react-dom": "^16.9.0", 52 | "react-native": "0.59.2", 53 | "react-test-renderer": "^16.9.0" 54 | }, 55 | "jest": { 56 | "preset": "react-native", 57 | "setupFiles": [ 58 | "./tests/setup.js" 59 | ], 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json" 63 | ], 64 | "transform": { 65 | "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js" 66 | }, 67 | "testMatch": [ 68 | "**/*.test.js" 69 | ], 70 | "snapshotSerializers": [ 71 | "./node_modules/enzyme-to-json/serializer" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/__snapshots__/example.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PortalExit contains ToPort component 1`] = ` 4 | 5 | 8 | 11 | 12 | 13 | `; 14 | 15 | exports[`mounts correctly 1`] = ` 16 | 17 | 20 | 23 | 26 | 27 | 28 | 29 | 30 | 33 | 36 | 37 | 38 | 39 | 40 | `; 41 | -------------------------------------------------------------------------------- /tests/__snapshots__/portal-enter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = `null`; 4 | -------------------------------------------------------------------------------- /tests/__snapshots__/portal-exit.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = `null`; 4 | -------------------------------------------------------------------------------- /tests/example.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { View } from "react-native"; 5 | import { mount } from "enzyme"; 6 | import React from "react"; 7 | 8 | import { PortalEnter, PortalExit, PortalProvider } from "../lib/index"; 9 | 10 | test("mounts correctly", () => { 11 | const ToPort = () => ; 12 | const App = ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | const wrapper = mount(App); 23 | 24 | expect(wrapper).toMatchSnapshot(); 25 | }); 26 | 27 | test("PortalExit contains ToPort component", () => { 28 | const ToPort = () => ; 29 | const App = ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | const wrapper = mount(App); 40 | 41 | expect(wrapper.find(PortalExit).find(ToPort)).toMatchSnapshot(); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/portal-enter.test.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import PortalEnter from '../lib/portal/portal-enter'; 4 | 5 | import renderer from 'react-test-renderer'; 6 | 7 | test('renders correctly', () => { 8 | const tree = renderer.create().toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); -------------------------------------------------------------------------------- /tests/portal-exit.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PortalExit from "../lib/portal/portal-exit"; 3 | 4 | import renderer from "react-test-renderer"; 5 | 6 | test("renders correctly", () => { 7 | const tree = renderer.create().toJSON(); 8 | expect(tree).toMatchSnapshot(); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | /** 5 | * Set up DOM in node.js environment for Enzyme to mount to 6 | */ 7 | const { JSDOM } = require('jsdom'); 8 | 9 | const jsdom = new JSDOM(''); 10 | const { window } = jsdom; 11 | 12 | function copyProps(src, target) { 13 | Object.defineProperties(target, { 14 | ...Object.getOwnPropertyDescriptors(src), 15 | ...Object.getOwnPropertyDescriptors(target), 16 | }); 17 | } 18 | 19 | global.window = window; 20 | global.document = window.document; 21 | global.navigator = { 22 | userAgent: 'node.js', 23 | }; 24 | copyProps(window, global); 25 | 26 | 27 | Enzyme.configure({ adapter: new Adapter() }); 28 | --------------------------------------------------------------------------------