├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Container.js ├── ContainerMixin.js ├── LICENSE ├── Layer.js ├── LayerMixin.js ├── Portal.js ├── README.md ├── __tests__ ├── Container-test.js ├── ContainerMixin-test.js ├── Layer-test.js ├── LayerMixin-test.js └── Portal-test.js ├── examples ├── README.md ├── dynamic-container │ ├── app.js │ └── index.html ├── global.css └── modal │ ├── app.js │ └── index.html ├── index.js ├── modules ├── Container.js ├── CustomPropTypes.js ├── ReactLayer.js ├── __tests__ │ ├── Container-test.js │ ├── CustomPropTypes-test.js │ └── ReactLayer-test.js └── documentBodyContainer.js ├── package.json ├── preprocessor.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | examples/_* 2 | examples/build 3 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower.json 2 | examples 3 | preprocessor.js 4 | __tests__ 5 | webpack.config.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | 3 | - Bugfix: Removed non referenced var from internal propType checks. 4 | 5 | ## 0.3.0 6 | 7 | - Added `ContainerMixin` and `Container` for mounting into react components. 8 | - Deprecate ability to pass react components as containers, use new `ContainerMixin` instead. 9 | - Converted whitespace to spaces! 10 | 11 | ## 0.2.0 12 | 13 | - Created `Portal` component which has the API of the previous `Layer` component. 14 | - Refactored the `Layer` component to accept a `layer` prop instead of using children. Children are passed to this components render function. 15 | 16 | ## 0.1.0 17 | 18 | - Initial Release. -------------------------------------------------------------------------------- /Container.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | 4 | var React = require('react'); 5 | var ContainerMixin = require('./ContainerMixin'); 6 | 7 | var Container = React.createClass({ 8 | mixins: [ContainerMixin], 9 | 10 | render: function() { 11 | // TODO: swap out to use ES6-7 spread operator when possible 12 | // @see https://gist.github.com/sebmarkbage/a6e220b7097eb3c79ab7 13 | // return
{this.props.children}{this.renderContainer()}
; 14 | return this.transferPropsTo( 15 |
16 | {this.props.children} 17 | 18 | {this.renderContainer()} 19 |
20 | ); 21 | } 22 | }); 23 | 24 | module.exports = Container; -------------------------------------------------------------------------------- /ContainerMixin.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | "use strict"; 3 | 4 | var React = require('react'); 5 | var Container = require('./modules/Container'); 6 | 7 | var ContainerMixin = { 8 | componentWillMount: function() { 9 | this.container = new Container(); 10 | }, 11 | 12 | componentDidMount: function() { 13 | if (!this.refs || !this.refs.container) { 14 | throw new Error( 15 | 'No container element found, check `' + 16 | (this.constructor.displayName || 'ReactCompositeComponent') + '.render()` ' + 17 | 'to make sure `this.renderContainer()` is being called.' 18 | ); 19 | } 20 | this.container.setContainer(this.refs.container.getDOMNode()); 21 | }, 22 | 23 | componentWillUnmount: function() { 24 | this.container.releaseContainer(); 25 | this.container = null; 26 | }, 27 | 28 | renderContainer: function() { 29 | return
; 30 | } 31 | }; 32 | 33 | module.exports = ContainerMixin; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Pieter Vanderwerff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Layer.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | "use strict"; 4 | 5 | var React = require('react'); 6 | var LayerMixin = require('./LayerMixin'); 7 | 8 | var Layer = React.createClass({ 9 | mixins: [LayerMixin], 10 | 11 | propTypes: { 12 | layer: React.PropTypes.component 13 | }, 14 | 15 | render: function() { 16 | // Extract out props used by this component. 17 | // TODO: swap out to use ES6-7 spread operator when possible 18 | // @see https://gist.github.com/sebmarkbage/a6e220b7097eb3c79ab7 19 | // var {container, layer, ...props} = this.props; 20 | // return
{this.props.children}
; 21 | return this.transferPropsTo(
{this.props.children}
); 22 | }, 23 | 24 | renderLayer: function() { 25 | return this.props.layer ? 26 | this.props.layer : null; 27 | } 28 | }); 29 | 30 | module.exports = Layer; -------------------------------------------------------------------------------- /LayerMixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var CustomPropTypes = require('./modules/CustomPropTypes'); 5 | var Container = require('./modules/Container'); 6 | var documentBodyContainer = require('./modules/documentBodyContainer'); 7 | var ReactLayer = require('./modules/ReactLayer'); 8 | 9 | function createContainer(container) { 10 | if (container.nodeType === 1) { 11 | return new Container(container); 12 | } 13 | 14 | // Handle extracting node from a React component 15 | // NOTE: this is deprecated behaviour and will be removed in 1.0 16 | if (container.getDOMNode) { 17 | return new Container(container.getDOMNode()); 18 | } 19 | 20 | return container; 21 | } 22 | 23 | var LayerMixin = { 24 | propTypes: { 25 | container: CustomPropTypes.mountable.isRequired 26 | }, 27 | 28 | getDefaultProps: function() { 29 | return { 30 | container: documentBodyContainer 31 | }; 32 | }, 33 | 34 | componentWillMount: function() { 35 | if (typeof this.renderLayer !== 'function') { 36 | throw new Error( 37 | 'createClass(...): Class specification of ' + 38 | (this.constructor.displayName || 'ReactCompositeComponent') + ' ' + 39 | 'using LayerMixin must implement a `renderLayer` method.' 40 | ); 41 | } 42 | }, 43 | 44 | componentDidMount: function() { 45 | this.__container = createContainer(this.props.container); 46 | this._updateLayer(); 47 | }, 48 | 49 | componentDidUpdate: function(prevProps) { 50 | if (this.props.container !== prevProps.container) { 51 | this._destroyLayer(); 52 | this.__container = createContainer(this.props.container); 53 | } 54 | 55 | this._updateLayer(); 56 | }, 57 | 58 | componentWillUnmount: function() { 59 | this._destroyLayer(); 60 | }, 61 | 62 | /** 63 | * Render layer 64 | * 65 | * To be implemented within your `React.createClass({})` 66 | */ 67 | // renderLayer() { return ; }, 68 | 69 | /** 70 | * Update layer 71 | * 72 | * @private 73 | */ 74 | _updateLayer: function() { 75 | var layer = this.renderLayer(); 76 | 77 | if (layer === null) { 78 | // No layer so just remove any existing components if they exist. 79 | this._destroyLayer(); 80 | return; 81 | } 82 | 83 | if (!React.isValidComponent(layer)) { 84 | throw new Error( 85 | (this.constructor.displayName || 'ReactCompositeComponent') + 86 | '.renderLayer(): A valid ReactComponent must be returned. You may have ' + 87 | 'returned undefined, an array or some other invalid object.' 88 | ); 89 | } 90 | 91 | if (!this.__layer) { 92 | this.__layer = new ReactLayer(); 93 | this.__container.addLayer(this.__layer); 94 | } 95 | 96 | this.__layer.render(layer); 97 | }, 98 | 99 | /** 100 | * Destroy layer 101 | * 102 | * Unmount component and remove container element from the DOM. 103 | * 104 | * @private 105 | */ 106 | _destroyLayer: function() { 107 | if (!this.__layer) { 108 | return; 109 | } 110 | 111 | this.__container.removeLayer(this.__layer); 112 | this.__layer = null; 113 | } 114 | }; 115 | 116 | module.exports = LayerMixin; -------------------------------------------------------------------------------- /Portal.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | "use strict"; 4 | 5 | var React = require('react'); 6 | var cloneWithProps = require('react/lib/cloneWithProps'); 7 | var LayerMixin = require('./LayerMixin'); 8 | 9 | var Portal = React.createClass({ 10 | mixins: [LayerMixin], 11 | 12 | render: function() { 13 | return null; 14 | }, 15 | 16 | renderLayer: function() { 17 | // Extract out props used by this component. 18 | // TODO: swap out to use ES6-7 spread operator when possible 19 | // @see https://gist.github.com/sebmarkbage/a6e220b7097eb3c79ab7 20 | // var {container, ...props} = this.props; 21 | // return
{this.props.children}
; 22 | return cloneWithProps(
{this.props.children}
, this.props); 23 | } 24 | }); 25 | 26 | module.exports = Portal; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Layers [![Build Status](https://travis-ci.org/pieterv/react-layers.svg?branch=master)](https://travis-ci.org/pieterv/react-layers) 2 | ============ 3 | 4 | A library for layering components in React. 5 | 6 | Important Notes 7 | --------------- 8 | 9 | This is an alpha release. The API and organizational structure are subject to 10 | change. Comments and contributions are much appreciated. 11 | 12 | Installation 13 | ------------ 14 | 15 | ```sh 16 | npm install react-layers 17 | ``` 18 | 19 | This library is written with CommonJS modules. If you are using 20 | browserify, webpack, or similar, you can consume it like anything else 21 | installed from npm. 22 | 23 | Modules 24 | -------- 25 | 26 | - LayerMixin 27 | - Layer 28 | - Portal 29 | - ContainerMixin 30 | - Container 31 | 32 | Check out the [`examples`](https://github.com/pieterv/react-layers/tree/master/examples) directory to see how these modules can be used. 33 | 34 | Thanks 35 | ------------- 36 | 37 | This library is highly inspired by the work done on the [react-bootstrap's `OverlayMixin`](https://github.com/react-bootstrap/react-bootstrap/blob/v0.11.1/src/OverlayMixin.js) and the discussion in [react issue #379](https://github.com/facebook/react/issues/379). 38 | -------------------------------------------------------------------------------- /__tests__/Container-test.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | jest.dontMock('../Container'); 4 | jest.dontMock('../Portal'); 5 | jest.dontMock('../ContainerMixin'); 6 | jest.dontMock('../LayerMixin'); 7 | jest.dontMock('../modules/Container'); 8 | jest.dontMock('../modules/ReactLayer'); 9 | 10 | describe('Container', function() { 11 | it('will render children into container', function() { 12 | var React = require('react'); 13 | var TestUtils = require('react/lib/ReactTestUtils'); 14 | var Portal = require('../Portal'); 15 | var Container = require('../Container'); 16 | 17 | var containerHolder = TestUtils.renderIntoDocument( 18 | 19 | Container text! 20 | 21 | ); 22 | 23 | TestUtils.renderIntoDocument( 24 | Hi over here 25 | ); 26 | var containerNode = containerHolder.getDOMNode(); 27 | expect(containerNode.firstChild).not.toBeNull(); 28 | expect(containerNode.children[1].firstChild.firstChild.id).toBe('test1'); 29 | expect(containerNode.children[1].firstChild.firstChild.textContent).toBe('Hi over here'); 30 | }); 31 | }); -------------------------------------------------------------------------------- /__tests__/ContainerMixin-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | jest.dontMock('../ContainerMixin'); 4 | 5 | describe('ContainerMixin', function() { 6 | it('should create container on will mount', function() { 7 | var ContainerMixin = require('../ContainerMixin'); 8 | var instance = Object.create(ContainerMixin); 9 | 10 | instance.componentWillMount(); 11 | expect(instance.container).toBeDefined(); 12 | expect(instance.container.setContainer).toBeDefined(); 13 | }); 14 | 15 | it('should throw if container not rendered', function() { 16 | var ContainerMixin = require('../ContainerMixin'); 17 | var instance = Object.create(ContainerMixin); 18 | 19 | instance.componentWillMount(); 20 | expect(instance.componentDidMount).toThrow(); 21 | }); 22 | 23 | it('should set container on did mount', function() { 24 | var ContainerMixin = require('../ContainerMixin'); 25 | var instance = Object.create(ContainerMixin); 26 | 27 | instance.componentWillMount(); 28 | var el = document.createElement('div'); 29 | var getDOMNode = jest.genMockFn().mockReturnValue(el); 30 | instance.refs = {container: {getDOMNode: getDOMNode}}; 31 | instance.componentDidMount(); 32 | expect(instance.container.setContainer).toBeCalledWith(getDOMNode); 33 | }); 34 | 35 | it('should release container on unmount', function() { 36 | var ContainerMixin = require('../ContainerMixin'); 37 | var instance = Object.create(ContainerMixin); 38 | 39 | instance.componentWillMount(); 40 | instance.refs = {container: {getDOMNode: jest.genMockFn()}}; 41 | instance.componentDidMount(); 42 | var container = instance.container; 43 | instance.componentWillUnmount(); 44 | expect(container.releaseContainer).toBeCalledWith(); 45 | expect(instance.container).toBeNull(); 46 | }); 47 | }); -------------------------------------------------------------------------------- /__tests__/Layer-test.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | jest.dontMock('../Layer'); 4 | jest.dontMock('../LayerMixin'); 5 | jest.dontMock('../modules/Container'); 6 | jest.dontMock('../modules/ReactLayer'); 7 | 8 | describe('Layer', function() { 9 | it('will render `props.layer` into container', function() { 10 | var React = require('react'); 11 | var TestUtils = require('react/lib/ReactTestUtils'); 12 | var Layer = require('../Layer'); 13 | 14 | var container = document.createElement('div'); 15 | var layerHolder = TestUtils.renderIntoDocument( 16 | }>hi! 17 | ); 18 | expect(container.firstChild).not.toBeNull(); 19 | expect(container.firstChild.firstChild.id).toBe('test1'); 20 | 21 | React.unmountComponentAtNode(layerHolder.getDOMNode().parentNode); 22 | expect(container.firstChild).toBeNull(); 23 | }); 24 | 25 | it('will not render `props.layer === null` layer', function() { 26 | var React = require('react'); 27 | var TestUtils = require('react/lib/ReactTestUtils'); 28 | var Layer = require('../Layer'); 29 | 30 | var container = document.createElement('div'); 31 | var layerHolder = TestUtils.renderIntoDocument( 32 | 33 | ); 34 | expect(container.firstChild).toBeNull(); 35 | }); 36 | 37 | it('will render children', function() { 38 | var React = require('react'); 39 | var TestUtils = require('react/lib/ReactTestUtils'); 40 | var Layer = require('../Layer'); 41 | 42 | var layerHolder = TestUtils.renderIntoDocument( 43 | 44 | 45 | 46 | ); 47 | expect(TestUtils.findRenderedDOMComponentWithClass(layerHolder, 'findme')).toBeDefined(); 48 | }); 49 | }); -------------------------------------------------------------------------------- /__tests__/LayerMixin-test.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | jest.dontMock('../LayerMixin'); 4 | jest.dontMock('../modules/Container'); 5 | jest.dontMock('../modules/ReactLayer'); 6 | 7 | describe('LayerMixin', function() { 8 | var LayerHolder; 9 | 10 | beforeEach(function() { 11 | var React = require('react'); 12 | var LayerMixin = require('../LayerMixin'); 13 | 14 | LayerHolder = React.createClass({ 15 | mixins: [LayerMixin], 16 | 17 | render: function() { 18 | return
; 19 | }, 20 | 21 | renderLayer: function() { 22 | return this.props.layer; 23 | } 24 | }); 25 | }); 26 | 27 | it('will mount to container default (document.body)', function() { 28 | var React = require('react'); 29 | var TestUtils = require('react/lib/ReactTestUtils'); 30 | var documentBodyContainer = require('../modules/documentBodyContainer'); 31 | 32 | var container = document.body; 33 | var layerHolder = TestUtils.renderIntoDocument( 34 | } /> 35 | ); 36 | expect(documentBodyContainer.addLayer).toBeCalled(); 37 | }); 38 | 39 | it('will mount to container DOM element', function() { 40 | var React = require('react'); 41 | var TestUtils = require('react/lib/ReactTestUtils'); 42 | 43 | var container = document.createElement('div'); 44 | var layerHolder = TestUtils.renderIntoDocument( 45 | } /> 46 | ); 47 | var layerElement = container.querySelector('#test1'); 48 | expect(layerElement).not.toBeNull(); 49 | expect(layerElement.parentNode.parentNode).toBe(container); 50 | }); 51 | 52 | it('will mount to react component', function() { 53 | var React = require('react'); 54 | var TestUtils = require('react/lib/ReactTestUtils'); 55 | 56 | var Holder = React.createClass({ 57 | render: function() { 58 | return ( 59 |
60 | } /> 61 |
62 | ); 63 | } 64 | }); 65 | 66 | var layerHolder = TestUtils.renderIntoDocument( 67 | 68 | ); 69 | 70 | var container = layerHolder.getDOMNode(); 71 | var layerElement = container.querySelector('#test1'); 72 | expect(layerElement).not.toBeNull(); 73 | expect(layerElement.parentNode.parentNode).toBe(container); 74 | }); 75 | 76 | it('will mount to Container', function() { 77 | var React = require('react'); 78 | var TestUtils = require('react/lib/ReactTestUtils'); 79 | var Container = require('../modules/Container'); 80 | 81 | var container = new Container(); 82 | container.addLayer = jest.genMockFn(); 83 | var layerHolder = TestUtils.renderIntoDocument( 84 | } /> 85 | ); 86 | expect(container.addLayer).toBeCalled(); 87 | }); 88 | 89 | it('will not mount to container with layer null', function() { 90 | var React = require('react'); 91 | var TestUtils = require('react/lib/ReactTestUtils'); 92 | 93 | var container = document.createElement('div'); 94 | var layerHolder = TestUtils.renderIntoDocument( 95 | 96 | ); 97 | expect(container.firstChild).toBeNull(); 98 | }); 99 | 100 | it('will unmount layer when not rendered', function() { 101 | var React = require('react'); 102 | var TestUtils = require('react/lib/ReactTestUtils'); 103 | 104 | var didMountSpy = jasmine.createSpy('didMount'); 105 | var willUnmountSpy = jasmine.createSpy('willUnmount'); 106 | var Layer = React.createClass({ 107 | render: function() { 108 | return
; 109 | }, 110 | 111 | componentDidMount: didMountSpy, 112 | componentWillUnmount: willUnmountSpy 113 | }); 114 | 115 | var container = document.createElement('div'); 116 | var holderContainer = document.createElement('div'); 117 | React.renderComponent( 118 | } />, 119 | holderContainer 120 | ); 121 | expect(didMountSpy).toHaveBeenCalled(); 122 | expect(willUnmountSpy).not.toHaveBeenCalled(); 123 | 124 | React.renderComponent( 125 | , 126 | holderContainer 127 | ); 128 | expect(willUnmountSpy).toHaveBeenCalled(); 129 | expect(container.firstChild).toBeNull(); 130 | }); 131 | 132 | it('will unmount layer when parent is unmounted', function() { 133 | var React = require('react'); 134 | var TestUtils = require('react/lib/ReactTestUtils'); 135 | 136 | var didMountSpy = jasmine.createSpy('didMount'); 137 | var willUnmountSpy = jasmine.createSpy('willUnmount'); 138 | var Layer = React.createClass({ 139 | render: function() { 140 | return
; 141 | }, 142 | 143 | componentDidMount: didMountSpy, 144 | componentWillUnmount: willUnmountSpy 145 | }); 146 | 147 | var container = document.createElement('div'); 148 | var holderContainer = document.createElement('div'); 149 | React.renderComponent( 150 | } />, 151 | holderContainer 152 | ); 153 | expect(didMountSpy).toHaveBeenCalled(); 154 | expect(willUnmountSpy).not.toHaveBeenCalled(); 155 | 156 | React.unmountComponentAtNode(holderContainer); 157 | expect(willUnmountSpy).toHaveBeenCalled(); 158 | expect(container.firstChild).toBeNull(); 159 | }); 160 | 161 | it('will remount when container is changed', function() { 162 | var React = require('react'); 163 | var TestUtils = require('react/lib/ReactTestUtils'); 164 | 165 | var didMountSpy = jasmine.createSpy('didMount'); 166 | var willUnmountSpy = jasmine.createSpy('willUnmount'); 167 | var Layer = React.createClass({ 168 | render: function() { 169 | return
; 170 | }, 171 | 172 | componentDidMount: didMountSpy, 173 | componentWillUnmount: willUnmountSpy 174 | }); 175 | 176 | var container = document.createElement('div'); 177 | var holderContainer = document.createElement('div'); 178 | React.renderComponent( 179 | } />, 180 | holderContainer 181 | ); 182 | expect(didMountSpy).toHaveBeenCalled(); 183 | expect(willUnmountSpy).not.toHaveBeenCalled(); 184 | 185 | var newContainer = document.createElement('div'); 186 | React.renderComponent( 187 | } />, 188 | holderContainer 189 | ); 190 | expect(willUnmountSpy).toHaveBeenCalled(); 191 | expect(didMountSpy.calls.length).toEqual(2); 192 | expect(container.firstChild).toBeNull(); 193 | expect(newContainer.firstChild).not.toBeNull(); 194 | }); 195 | 196 | describe('Errors', function() { 197 | it('will throw when render returns not a valid component or null', function() { 198 | var React = require('react'); 199 | var TestUtils = require('react/lib/ReactTestUtils'); 200 | 201 | function render(layer) { 202 | TestUtils.renderIntoDocument( 203 | 204 | ); 205 | } 206 | 207 | var errorMessage = ( 208 | 'LayerHolder.renderLayer(): A valid ReactComponent must be returned. You may have ' + 209 | 'returned undefined, an array or some other invalid object.' 210 | ); 211 | 212 | expect(render.bind(null, 'a string')).toThrow(errorMessage); 213 | expect(render.bind(null, undefined)).toThrow(errorMessage); 214 | expect(render.bind(null, {})).toThrow(errorMessage); 215 | expect(render.bind(null, [])).toThrow(errorMessage); 216 | expect(render.bind(null, null)).not.toThrow(); 217 | expect(render.bind(null,
)).not.toThrow(); 218 | }); 219 | 220 | it('will throw when layerRender is not defined', function() { 221 | var React = require('react'); 222 | var TestUtils = require('react/lib/ReactTestUtils'); 223 | var LayerMixin = require('../LayerMixin'); 224 | 225 | var BadLayerHolder = React.createClass({ 226 | mixins: [LayerMixin], 227 | 228 | render: function() { 229 | return
; 230 | } 231 | }); 232 | 233 | function render() { 234 | TestUtils.renderIntoDocument( 235 | 236 | ); 237 | } 238 | 239 | var errorMessage = ( 240 | 'createClass(...): Class specification of BadLayerHolder using LayerMixin must ' + 241 | 'implement a `renderLayer` method.' 242 | ); 243 | 244 | expect(render).toThrow(errorMessage); 245 | }); 246 | }); 247 | }); -------------------------------------------------------------------------------- /__tests__/Portal-test.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | jest.dontMock('../Portal'); 4 | jest.dontMock('../LayerMixin'); 5 | jest.dontMock('../modules/Container'); 6 | jest.dontMock('../modules/ReactLayer'); 7 | 8 | describe('Portal', function() { 9 | it('will render children into container', function() { 10 | var React = require('react'); 11 | var TestUtils = require('react/lib/ReactTestUtils'); 12 | var Portal = require('../Portal'); 13 | 14 | var container = document.createElement('div'); 15 | var portalHolder = TestUtils.renderIntoDocument( 16 | Hi over here 17 | ); 18 | expect(container.firstChild).not.toBeNull(); 19 | expect(container.firstChild.firstChild.id).toBe('test1'); 20 | expect(container.firstChild.firstChild.textContent).toBe('Hi over here'); 21 | }); 22 | }); -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ### React Layers Examples 2 | 3 | In order to try out the examples, you need to follow these steps: 4 | 5 | 1. Clone this repo 6 | 1. Run `npm install` from the repo's root directory 7 | 1. Run `npm run build-examples` from the repo's root directory 8 | 1. Open the examples `index.html` file in your browser -------------------------------------------------------------------------------- /examples/dynamic-container/app.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | var ReactLayers = require('../../'); 5 | var ReactLayer = ReactLayers.Layer; 6 | var ReactContainerMixin = ReactLayers.ContainerMixin; 7 | 8 | var NestedLayer = React.createClass({ 9 | mixins: [ReactContainerMixin], 10 | 11 | getInitialState: function() { 12 | return { 13 | hasLayer: false 14 | }; 15 | }, 16 | 17 | render: function() { 18 | var layerStyle = { 19 | position: 'absolute', 20 | top: 10, 21 | right: 10, 22 | bottom: 10, 23 | left: 10, 24 | backgroundColor: '#fff', 25 | textAlign: 'center', 26 | border: '1px solid red' 27 | }; 28 | 29 | return ( 30 |
31 |

Dynamic container

32 | 33 | 34 | 35 | : null} /> 36 | 37 | {this.renderContainer()} 38 |
39 | ); 40 | }, 41 | 42 | handleShowLayer: function() { 43 | this.setState({hasLayer: true}); 44 | } 45 | }); 46 | 47 | React.renderComponent(, document.getElementById('example')); -------------------------------------------------------------------------------- /examples/dynamic-container/index.html: -------------------------------------------------------------------------------- 1 | 2 | Dynamic Container Example 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | text-align: center; 5 | } 6 | -------------------------------------------------------------------------------- /examples/modal/app.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | var ReactLayerMixin = require('../../').LayerMixin; 5 | 6 | var ModalSection = React.createClass({ 7 | mixins: [ReactLayerMixin], 8 | 9 | getInitialState: function() { 10 | return { 11 | isModalActive: false 12 | }; 13 | }, 14 | 15 | render: function() { 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }, 22 | 23 | renderLayer: function() { 24 | if (!this.state.isModalActive) { 25 | return null; 26 | } 27 | 28 | var layerStyle = { 29 | position: 'fixed', 30 | top: 0, 31 | right: 0, 32 | bottom: 0, 33 | left: 0, 34 | backgroundColor: '#fff', 35 | textAlign: 'center', 36 | border: '1px solid red' 37 | }; 38 | 39 | return ( 40 |
41 |

Hi there!

42 | 43 |
44 | ); 45 | }, 46 | 47 | handleOpenModal: function() { 48 | this.setState({isModalActive: true}); 49 | }, 50 | 51 | handleCloseModal: function() { 52 | this.setState({isModalActive: false}); 53 | } 54 | }); 55 | 56 | React.renderComponent(, document.getElementById('example')); -------------------------------------------------------------------------------- /examples/modal/index.html: -------------------------------------------------------------------------------- 1 | 2 | Modal Example 3 | 4 | 5 |

Modal

6 |
7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.LayerMixin = require('./LayerMixin'); 4 | exports.Layer = require('./Layer'); 5 | exports.Portal = require('./Portal'); 6 | exports.ContainerMixin = require('./ContainerMixin'); 7 | exports.Container = require('./Container'); -------------------------------------------------------------------------------- /modules/Container.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Container 5 | * 6 | * @param {HTMLElement?} container 7 | * @constructor 8 | */ 9 | function Container(container) { 10 | this.__layers = []; 11 | if (container) { 12 | this.setContainer(container); 13 | } 14 | } 15 | 16 | /** 17 | * Set the container element 18 | * 19 | * Will remove all existing layers and add them to the new container. 20 | * 21 | * @param {HTMLElement?} container 22 | */ 23 | Container.prototype.setContainer = function(container) { 24 | var layers = this.__layers.slice(); 25 | if (this.__container != null) { 26 | layers.forEach(this.removeLayer, this); 27 | } else if (layers.length) { 28 | this.__layers = []; 29 | } 30 | 31 | this.__container = container; 32 | layers.forEach(this.addLayer, this); 33 | }; 34 | 35 | /** 36 | * Release container 37 | * 38 | * Remove all mounted layers and return the container element. 39 | * 40 | * @returns {HTMLElement|null} 41 | */ 42 | Container.prototype.releaseContainer = function() { 43 | var container = this.__container; 44 | this.setContainer(null); 45 | return container; 46 | }; 47 | 48 | /** 49 | * Add a layer to this container 50 | * 51 | * Will mount the layer if a container is set. 52 | * 53 | * @param {ReactLayer} layer 54 | */ 55 | Container.prototype.addLayer = function(layer) { 56 | this.__layers.push(layer); 57 | 58 | if (this.__container) { 59 | var layerContainer = document.createElement('div'); 60 | this.__container.appendChild(layerContainer); 61 | 62 | layer.setContainer(layerContainer); 63 | } 64 | }; 65 | 66 | /** 67 | * Remove a layer from the container 68 | * 69 | * @param {ReactLayer} layer 70 | */ 71 | Container.prototype.removeLayer = function(layer) { 72 | var layerIndex = this.__layers.indexOf(layer); 73 | 74 | if (layerIndex === -1) { 75 | throw new Error('Layer does not belong to this container'); 76 | } 77 | 78 | var layerContainer = layer.releaseContainer(); 79 | this.__layers.splice(layerIndex, 1); 80 | 81 | if (this.__container) { 82 | this.__container.removeChild(layerContainer); 83 | } 84 | }; 85 | 86 | module.exports = Container; -------------------------------------------------------------------------------- /modules/CustomPropTypes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Container = require('./Container'); 4 | 5 | var ANONYMOUS = '<>'; 6 | 7 | /** 8 | * Checks whether a prop provides a DOM element 9 | * 10 | * The element can be provided in two forms: 11 | * - Directly passed 12 | * - Or passed an object which has a `getDOMNode` method which will return the required DOM element 13 | * 14 | * @param props 15 | * @param propName 16 | * @param componentName 17 | * @returns {Error|undefined} 18 | */ 19 | exports.mountable = createMountableChecker(); 20 | 21 | function createMountableChecker() { 22 | function validate(props, propName, componentName) { 23 | if (typeof props[propName] === 'object') { 24 | if (props[propName].nodeType === 1) { 25 | return; // Valid 26 | } 27 | 28 | if (props[propName] instanceof Container) { 29 | return; // Valid 30 | } 31 | 32 | if (typeof props[propName].getDOMNode === 'function') { 33 | // Will still actually work but is deprecated 34 | return new Error( 35 | 'Prop `' + propName + '` supplied to `' + componentName + '` was passed a React component, ' + 36 | 'this is a deprecated behaviour and will be removed. Please use the `ContainerMixin` or ' + 37 | '`Container` component and pass `this.container` instead.' 38 | ); 39 | } 40 | } 41 | 42 | return new Error( 43 | 'Invalid prop `' + propName + '` supplied to `' + componentName + '`, ' + 44 | 'expected a HTMLElement or Container instance.' 45 | ); 46 | } 47 | 48 | return createChainableTypeChecker(validate); 49 | } 50 | 51 | /** 52 | * Create chain-able isRequired validator 53 | * 54 | * Largely copied directly from: 55 | * https://github.com/facebook/react/blob/0.11-stable/src/core/ReactPropTypes.js#L94 56 | */ 57 | function createChainableTypeChecker(validate) { 58 | function checkType(isRequired, props, propName, componentName) { 59 | componentName = componentName || ANONYMOUS; 60 | if (props[propName] == null) { 61 | if (isRequired) { 62 | return new Error( 63 | 'Required prop `' + propName + '` was not specified in ' + 64 | '`' + componentName + '`.' 65 | ); 66 | } 67 | } else { 68 | return validate(props, propName, componentName); 69 | } 70 | } 71 | 72 | var chainedCheckType = checkType.bind(null, false); 73 | chainedCheckType.isRequired = checkType.bind(null, true); 74 | 75 | return chainedCheckType; 76 | } -------------------------------------------------------------------------------- /modules/ReactLayer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | 5 | /** 6 | * React Layer 7 | * 8 | * Handles the rendering and un-mounting of layers. 9 | * 10 | * @param {HTMLElement} container 11 | * @constructor 12 | */ 13 | function ReactLayer(container) { 14 | if (container) { 15 | this.setContainer(container); 16 | } 17 | } 18 | 19 | /** 20 | * Render into layer 21 | * 22 | * @param {ReactDescriptor} nextDescriptor 23 | */ 24 | ReactLayer.prototype.render = function(nextDescriptor) { 25 | this.__nextDescriptor = nextDescriptor; 26 | if (this.__container) { 27 | React.renderComponent(nextDescriptor, this.__container); 28 | } 29 | }; 30 | 31 | /** 32 | * Set layer container 33 | * 34 | * Will render into layer if a render is queued. 35 | * 36 | * @param {HTMLElement} container 37 | */ 38 | ReactLayer.prototype.setContainer = function(container) { 39 | this.__container = container; 40 | if (this.__nextDescriptor) { 41 | this.render(this.__nextDescriptor); 42 | } 43 | }; 44 | 45 | /** 46 | * Will unmount React component and return container element 47 | * 48 | * @returns {HTMLElement|null} 49 | */ 50 | ReactLayer.prototype.releaseContainer = function() { 51 | var container = this.__container; 52 | if (!container) { 53 | return null; 54 | } 55 | 56 | if (this.__nextDescriptor) { 57 | React.unmountComponentAtNode(container); 58 | this.__nextDescriptor = null; 59 | } 60 | 61 | this.__container = null; 62 | return container; 63 | }; 64 | 65 | module.exports = ReactLayer; -------------------------------------------------------------------------------- /modules/__tests__/Container-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | jest.dontMock('../Container'); 4 | 5 | describe('Container', function() { 6 | it('should create', function() { 7 | var Container = require('../Container'); 8 | var container1 = new Container(); 9 | expect(container1).toBeDefined(); 10 | 11 | container1 = new Container(document.createElement('div')); 12 | expect(container1).toBeDefined(); 13 | }); 14 | 15 | it('should set container', function() { 16 | var Container = require('../Container'); 17 | var ReactLayer = require('../ReactLayer'); 18 | var container1 = new Container(); 19 | 20 | var layer = new ReactLayer(); 21 | container1.addLayer(layer); 22 | expect(layer.setContainer).not.toBeCalled(); 23 | var el = document.createElement('div'); 24 | container1.setContainer(el); 25 | expect(layer.setContainer).toBeCalledWith(el.firstChild); 26 | }); 27 | 28 | it('should add layers', function() { 29 | var Container = require('../Container'); 30 | var ReactLayer = require('../ReactLayer'); 31 | var el = document.createElement('div'); 32 | var container1 = new Container(el); 33 | 34 | var layer = new ReactLayer(); 35 | container1.addLayer(layer); 36 | expect(layer.setContainer).toBeCalledWith(el.firstChild); 37 | }); 38 | 39 | it('should remove layers', function() { 40 | var Container = require('../Container'); 41 | var ReactLayer = require('../ReactLayer'); 42 | var el = document.createElement('div'); 43 | var container1 = new Container(el); 44 | 45 | var layer = new ReactLayer(); 46 | layer.setContainer.mockImpl(function(layerEl) { 47 | layer.releaseContainer.mockReturnValue(layerEl); 48 | }); 49 | container1.addLayer(layer); 50 | container1.removeLayer(layer); 51 | expect(layer.releaseContainer).toBeCalled(); 52 | expect(el.firstChild).toBeNull(); 53 | }); 54 | 55 | it('should remove layers on release container', function() { 56 | var Container = require('../Container'); 57 | var ReactLayer = require('../ReactLayer'); 58 | var el = document.createElement('div'); 59 | var container1 = new Container(el); 60 | 61 | var layer = new ReactLayer(); 62 | layer.setContainer.mockImpl(function(layerEl) { 63 | layer.releaseContainer.mockReturnValue(layerEl); 64 | }); 65 | 66 | container1.addLayer(layer); 67 | var returnVal = container1.releaseContainer(); 68 | expect(layer.releaseContainer).toBeCalled(); 69 | expect(returnVal).toBe(el); 70 | expect(el.firstChild).toBeNull(); 71 | }); 72 | }); -------------------------------------------------------------------------------- /modules/__tests__/CustomPropTypes-test.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | jest.dontMock('../CustomPropTypes'); 4 | 5 | describe('CustomPropTypes', function() { 6 | describe('Mountable', function() { 7 | var validate, validateRequired; 8 | 9 | beforeEach(function() { 10 | var CustomPropTypes = require('../CustomPropTypes'); 11 | 12 | validate = function validate(prop) { 13 | return CustomPropTypes.mountable({p: prop}, 'p', 'Component'); 14 | }; 15 | validateRequired = function validateRequired(prop) { 16 | return CustomPropTypes.mountable.isRequired({p: prop}, 'p', 'Component'); 17 | }; 18 | }); 19 | 20 | it('Should return error with non mountable values', function() { 21 | var React = require('react'); 22 | var TestUtils = require('react/lib/ReactTestUtils'); 23 | 24 | expect(validateRequired()).toEqual(jasmine.any(Error)); 25 | expect(validateRequired(null)).toEqual(jasmine.any(Error)); 26 | expect(validate({})).toEqual(jasmine.any(Error)); 27 | expect(validate(TestUtils.renderIntoDocument(
))).toEqual(jasmine.any(Error)); 28 | }); 29 | 30 | it('Should return undefined with mountable values', function() { 31 | expect(validate()).toBeUndefined(); 32 | expect(validate(null)).toBeUndefined(); 33 | expect(validate(document.createElement('div'))).toBeUndefined(); 34 | expect(validate(document.body)).toBeUndefined(); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /modules/__tests__/ReactLayer-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | jest.dontMock('../ReactLayer'); 4 | jest.mock('react'); 5 | 6 | describe('ReactLayer', function() { 7 | it('should create', function() { 8 | var ReactLayer = require('../ReactLayer'); 9 | var layer = new ReactLayer(); 10 | expect(layer).toBeDefined(); 11 | 12 | layer = new ReactLayer(document.createElement('div')); 13 | expect(layer).toBeDefined(); 14 | }); 15 | 16 | it('should render on set container with descriptor', function() { 17 | var ReactLayer = require('../ReactLayer'); 18 | var React = require('react'); 19 | var layer = new ReactLayer(); 20 | 21 | var fakeDescriptor = {}; 22 | layer.render(fakeDescriptor); 23 | expect(React.renderComponent).not.toBeCalled(); 24 | 25 | var el = document.createElement('div'); 26 | layer.setContainer(el); 27 | expect(React.renderComponent).toBeCalledWith(fakeDescriptor, el); 28 | }); 29 | 30 | it('should render with container', function() { 31 | var ReactLayer = require('../ReactLayer'); 32 | var React = require('react'); 33 | 34 | var el = document.createElement('div'); 35 | var layer = new ReactLayer(el); 36 | 37 | var fakeDescriptor = {}; 38 | layer.render(fakeDescriptor); 39 | expect(React.renderComponent).toBeCalledWith(fakeDescriptor, el); 40 | }); 41 | 42 | it('should release container', function() { 43 | var ReactLayer = require('../ReactLayer'); 44 | var React = require('react'); 45 | 46 | var el = document.createElement('div'); 47 | var layer = new ReactLayer(el); 48 | 49 | expect(layer.releaseContainer()).toBe(el); 50 | expect(React.unmountComponentAtNode).not.toBeCalled(); 51 | }); 52 | 53 | it('should unmount container when release if rendered', function() { 54 | var ReactLayer = require('../ReactLayer'); 55 | var React = require('react'); 56 | 57 | var el = document.createElement('div'); 58 | var layer = new ReactLayer(el); 59 | 60 | var fakeDescriptor = {}; 61 | layer.render(fakeDescriptor); 62 | 63 | expect(layer.releaseContainer()).toBe(el); 64 | expect(React.unmountComponentAtNode).toBeCalledWith(el); 65 | }); 66 | }); -------------------------------------------------------------------------------- /modules/documentBodyContainer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Container = require('./Container'); 4 | 5 | /** 6 | * Create singleton 7 | * 8 | * @type {Container} 9 | */ 10 | var documentBodyContainer = new Container(); 11 | 12 | // Setup DOM ready 13 | if (typeof document !== 'undefined') { 14 | if (document.addEventListener) { 15 | document.addEventListener('DOMContentLoaded', onDOMReady); 16 | } else { 17 | document.attachEvent('onreadystatechange', function() { 18 | if (document.readyState === 'interactive') { 19 | onDOMReady(); 20 | } 21 | }); 22 | } 23 | } 24 | 25 | function onDOMReady() { 26 | // The `document.body` reference needs to be contained within this 27 | // function so that it is not accessed in environments where it 28 | // would not be defined, e.g. nodejs. This provider will only be 29 | // accessed after componentDidMount. 30 | documentBodyContainer.setContainer(document.body); 31 | } 32 | 33 | module.exports = documentBodyContainer; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-layers", 3 | "version": "0.3.1", 4 | "description": "A library for layering components in React", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "layers", 9 | "layering", 10 | "overlay" 11 | ], 12 | "main": "index.js", 13 | "scripts": { 14 | "test": "jest", 15 | "build-examples": "webpack --devtool inline-source-map", 16 | "watch-examples": "webpack --devtool inline-source-map --watch" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/pieterv/react-layers.git" 21 | }, 22 | "homepage": "https://github.com/pieterv/react-layers", 23 | "bugs": "https://github.com/pieterv/react-layers/issues", 24 | "author": "Pieter Vanderwerff", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "jest-cli": "^0.1.18", 28 | "jsx-loader": "^0.11.0", 29 | "react-tools": "^0.11.1", 30 | "webpack": "^1.3.7", 31 | "react": ">=0.11.0" 32 | }, 33 | "peerDependencies": { 34 | "react": ">=0.11.0" 35 | }, 36 | "jest": { 37 | "scriptPreprocessor": "/preprocessor.js", 38 | "unmockedModulePathPatterns": [ 39 | "/node_modules/react" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | var ReactTools = require('react-tools'); 2 | 3 | module.exports = { 4 | process: function(src) { 5 | return ReactTools.transform(src, {sourceMap: true}); 6 | } 7 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | function buildEntries() { 5 | return fs.readdirSync('examples').reduce(function(entries, dir) { 6 | if (dir === 'build') { 7 | return entries; 8 | } 9 | var isDraft = dir.charAt(0) === '_'; 10 | if (!isDraft && fs.lstatSync(path.join('examples', dir)).isDirectory()) { 11 | entries[dir] = './examples/' + dir + '/' + 'app.js'; 12 | } 13 | return entries; 14 | }, {}); 15 | } 16 | 17 | module.exports = { 18 | entry: buildEntries(), 19 | 20 | output: { 21 | filename: '[name].js', 22 | chunkFilename: '[id].chunk.js', 23 | path: path.join(__dirname, 'examples', 'build'), 24 | publicPath: '../build/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx-loader'} 30 | ] 31 | } 32 | }; --------------------------------------------------------------------------------