Skip to content

Commit

Permalink
Initial release, with demo and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
davidtheclark committed Aug 26, 2015
1 parent 76ccd99 commit 75b3b47
Show file tree
Hide file tree
Showing 18 changed files with 804 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintignore
@@ -0,0 +1 @@
**/*-bundle.js
30 changes: 30 additions & 0 deletions .eslintrc
@@ -0,0 +1,30 @@
{
"env": {
"node": true,
"browser": true
},
"ecmaFeatures": {
"jsx": true,
},
"rules": {
"comma-dangle": [2, "always-multiline"],
"curly": 0,
"radix": 2,
"wrap-iife": 2,
"brace-style": 0,
"comma-style": 2,
"consistent-this": 0,
"indent": [2, 2, {
"SwitchCase": 1
}],
"no-lonely-if": 2,
"no-nested-ternary": 2,
"no-use-before-define": [2, "nofunc"],
"quotes": [2, "single"],
"space-before-function-paren": [2, "never"],
"space-after-keywords": [2, "always"],
"space-before-blocks": [2, "always"],
"space-in-parens": [2, "never"],
"space-unary-ops": 2,
}
}
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
node_modules
*.log
**/*-bundle.js
3 changes: 3 additions & 0 deletions .travis.yml
@@ -0,0 +1,3 @@
language: node_js
node_js:
- "0.12"
3 changes: 3 additions & 0 deletions CHANGELOG
@@ -0,0 +1,3 @@
# 1.0.0

- Initial release.
112 changes: 111 additions & 1 deletion README.md
@@ -1,2 +1,112 @@
# react-displace
A higher order component that displaces your component into a remote region of the DOM

A higher order component that displaces *your* component into a remote region of the DOM. When your component mounts, it renders to the `document.body` or to any other arbitrary DOM node, instead of it's expected place within its React component tree; but it still maintains its normal life cycle within the tree, mounting, updating, and unmounting as expected.

This is useful when the HTML source order enforced by React's component tree won't serve your purposes.

For example, if initialization and props for a modal or an obstructive overlay (e.g. "Loading...") will come from some component deeply nested within you app, but you want to render the modal or overlay as a direct child of `document.body` so that you can easily `position` it and set its `z-index`.

## Installation

```
npm install react-displace
```

## Usage

react-displace is a "higher order component": a function that takes your component as an argument and returns a new component that includes your component wrapped in some special functionality.

It has a simple signature

```js
displace(YourComponent[, options])
```

### Options

#### renderTo

Type: DOM node or string selector

By default, the displaced component is appended to a new `<div>` attached directly to `document.body`. If instead you would like to specify a node that the component should be displaced to, do that with `renderTo`.

If `renderTo` is a DOM node, the displaced component will be rendered there.

If `renderTo` is a selector string, it is passed to `document.querySelector()`, and the displaced component will be rendered to that result.

### Example

```js
var React = require('react');
var displace = require('react-displace');

var Foo = React.createClass({ .. });
var FooDisplacedToBody = displace(Foo);
var FooDisplacedToBar = displace(Foo, document.getElementById('bar'));
var FooDisplacedToBaz = displace(Foo, '#baz');
```

In the example above, you can use any `FooDisplacedTo*` exactly as you would use `Foo`; and any `props` you provide to `FooDisplacedTo*` will be passed through to its internal `Foo` component. (e.g. If `Foo` has a `prop` called `severity`, so does `FooDisplacedTo*`.)

The only differences are that all of the `FooDisplacedTo*` components will be rendered to some special place in the DOM, instead of being inserted wherever it is used within the React component tree.
- `FooDisplacedToBody` will be rendered into a new `<div>` appended directly to `document.body`,
- `FooDisplacedToBar` and `FooDisplacedToBaz` will be appended into their designated containers.

The `FooDisplacedTo*` components will also have an additional `prop`: `mounted`. The `mounted` prop can be used to declare whether the component should be rendered or not — which can also be done by actually mounting and unmounting the component.

So let's say you have the following HTML:

```html
<div id="app-container"></div>
<div id="bar"></div>
<div id="baz"></div>
```

And you have something like the following JS:

```js
var React = require('react');
var displace = require('react-displace');

var Foo = React.createClass({ .. });
var FooDisplacedToBody = displace(Foo);
var FooDisplacedToBar = displace(Foo, document.getElementById('bar'));
var FooDisplacedToBaz = displace(Foo, '#baz');

var App = React.createClass({
..
render: function() {
return (
<div id="rendered-app">
<Foo text='in my normal place' />
<FooDisplacedToBody text='displaced to body' />
<FooDisplacedToBar text='displaced to bar' />
<FooDisplacedToBaz text='displaced to baz' />
</div>
);
},
})
```

What ends up rendering should look something like this:

```html
<div id="app-container">
<div id="rendered-app">
<div>in my normal place</div>
</div>
</div>
<div id="bar">
<div>displaced to bar</div>
</div>
<div id="baz">
<div>displaced to baz</div>
</div>
<div>
<div>displaced to body</div>
</div>
```

## Caveats

I don't think `React.findDOMNode()` always works on the displaced element — which is not surprising.
85 changes: 85 additions & 0 deletions demo/index.html
@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>react-displace demo</title>

<link rel="stylesheet" href="style.css">
<meta name="description" content="A demo of react-displace">
</head>
<body>

<h1>
react-displace demo
</h1>

<p>
react-displace is a higher order function that makes any given React component render into the document's <code>body</code> or some other arbitrary DOM node instead of into its normal place within the React element tree.
</p>

<p>
<span style='font-size:2em;vertical-align:middle;'>&#9756;</span>
<a href="https://github.com/davidtheclark/react-displace" style='vertical-align:middle;'>Return to the repository.</a>
</p>

<div class="container">
<h2>
React apps are attached within here
</h2>
<p>
But you'll see that they control (mount, unmount, and update the state of) React elements <em>outside</em> this DOM node.
</p>
<div class="container">
<h3>
Demo One
</h3>
<p>
This displaced element should appear at the bottom of the page.
</p>
<div id="demo-one"></div>
</div>
<div class="container">
<h3>
Demo Two
</h3>
<p>
This displaced element should appear in the Demo Two Displacement Container.
</p>
<div id="demo-two"></div>
</div>
<div class="container">
<h3>
Demo Three
</h3>
<p>
This displaced element should appear in the Demo Three Displacement Container.
</p>
<div id="demo-three"></div>
</div>
</div>

<div class="container">
<h2>
Demo Two Displacement Container
</h2>
<p>
demo-two's displaced element will be attached in here
</p>
<div id="demo-two-displaced"></div>
</div>

<div class="container">
<h2>
Demo Three Displacement Container
</h2>
<p>
demo-three's displaced element will be attached in here
</p>
<div id="demo-three-displaced"></div>
</div>

<script src="demo-bundle.js"></script>
</body>
</html>
61 changes: 61 additions & 0 deletions demo/js/demo-one.jsx
@@ -0,0 +1,61 @@
var React = require('react');
var displace = require('../../');

var AppendedToBody = displace(React.createClass({
propTypes: {
number: React.PropTypes.number.isRequired,
},

render: function() {
return (
<div className='container'>
<h2>
Demo One's Displaced Element
</h2>
<p>
I'm appended to the body rather than my parent React element.
</p>
<p>
And I still update correctly.
You've clicked "increment demo-one displaced number" {this.props.number} time(s).
</p>
</div>
)
},
}));

var DemoOne = React.createClass({
getInitialState: function() {
return {
displacedNumber: 0,
displacedMounted: false,
};
},

toggleDisplaced: function() {
this.setState({ displacedMounted: !this.state.displacedMounted });
},

incrementDisplaced: function() {
this.setState({ displacedNumber: this.state.displacedNumber + 1 });
},

render: function() {
return (
<div>
<button onClick={this.toggleDisplaced}>
toggle demo-one displaced
</button>
<button onClick={this.incrementDisplaced}>
increment demo-one displaced number
</button>
<AppendedToBody
number={this.state.displacedNumber}
mounted={this.state.displacedMounted}
/>
</div>
);
},
});

React.render(<DemoOne />, document.getElementById('demo-one'));
65 changes: 65 additions & 0 deletions demo/js/demo-three.jsx
@@ -0,0 +1,65 @@
var React = require('react');
var displace = require('../../');

var AppendedToNodeInner = React.createClass({
propTypes: {
number: React.PropTypes.number.isRequired,
},

render: function() {
return (
<div className='container'>
<h3>
Demo Three's Displaced Element
</h3>
<p>
I'm appended to a specific node that is passed as a node (not selector string).
</p>
<p>
And I still update correctly.
You've clicked "increment demo-three displaced number" {this.props.number} time(s).
</p>
</div>
)
},
});

var AppendedToNode = displace(AppendedToNodeInner, {
renderTo: document.getElementById('demo-three-displaced'),
});

var DemoThree = React.createClass({
getInitialState: function() {
return {
displacedNumber: 0,
displacedMounted: false,
};
},

toggleDisplaced: function() {
this.setState({ displacedMounted: !this.state.displacedMounted });
},

incrementDisplaced: function() {
this.setState({ displacedNumber: this.state.displacedNumber + 1 });
},

render: function() {
return (
<div>
<button onClick={this.toggleDisplaced}>
toggle demo-three displaced
</button>
<button onClick={this.incrementDisplaced}>
increment demo-three displaced number
</button>
<AppendedToNode
number={this.state.displacedNumber}
mounted={this.state.displacedMounted}
/>
</div>
);
},
});

React.render(<DemoThree />, document.getElementById('demo-three'));

0 comments on commit 75b3b47

Please sign in to comment.