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 [](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 |