Skip to content

Commit

Permalink
Adding a React Component for establishing Context via React Trees [v2] (
Browse files Browse the repository at this point in the history
#632)

* Add DragDropContextProvider React Class

* Correct PropTypes, inject document via ContextType

* Remave getDocument method, get window directly from context instead of through document

* Remove react-dom include

* Avoid using variable names that shadow browser globals

* Update Documentation with Simple iframe Example

* Add some more detail to the iframe exmaple

* Correct linting issue

* Update the container in the iframe example to reference components from the simple example

* Add documentation to the DragDropContextProvider
  • Loading branch information
darthtrevino committed Jan 26, 2017
1 parent 919da76 commit 1153d82
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 31 deletions.
73 changes: 73 additions & 0 deletions docs/01 Top Level API/DragDropContextProvider.md
@@ -0,0 +1,73 @@
*New to React DnD? [Read the overview](docs-overview.html) before jumping into the docs.*

DragDropContextProvider
=========================

As an alternative to the DragDropContext, you can use the DragDropContextProvider element
to set up React DnD for your application. Similar to the DragDropContext, this may be
injected with a backend via the `backend` prop, but it also can be injected with a `window` object.

### Usage

-------------------
```js
var HTML5Backend = require('react-dnd-html5-backend');
var DragDropContextProvider = require('react-dnd').DragDropContextProvider;

var YourApp = React.createClass(
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
/* ... */
</DragDropContextProvider>
);
};
);

module.exports = YourApp;
```
-------------------
```js
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContextProvider } from 'react-dnd';

export default class YourApp {
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
/* ... */
</DragDropContextProvider>
);
};
}

```
-------------------
```js
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContextProvider } from 'react-dnd';

export default class YourApp {
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
/* ... */
</DragDropContextProvider>
);
};
}
```
-------------------

### Props

* **`backend`**: Required. A React DnD backend. Unless you're writing a custom one, you probably want to use the [HTML5 backend](docs-html5-backend.html) that ships with React DnD.
* **`window`**: Optional. The window object used for establishing subscriptions in the HTML5 Backend. This is mainly for iframe support.

### Injecting a Window Instance (optional)
In order to support iframes, we need to be able to inject the window we're subscribing for events in into the HTML5 Backend. You can do this in a couple of ways.

* Via the `window` prop. This has the highest precedent for determining the window to use.
* Via the `window` context value. This has the next higest precedent.

If neither of these arguments are present, then the global `window` variable is used.
32 changes: 32 additions & 0 deletions examples/01 Dustbin/Single Target in iframe/Container.js
@@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Frame from 'react-frame-component';
import Dustbin from '../Single Target/Dustbin';
import Box from '../Single Target/Box';

// Don't use the decorator, embed the DnD context within the iframe
export default class Container extends Component {
render() {
// The react-frame-component will pass the iframe's 'window' global as a context value
// to the DragDropContext provider. You could also directly inject it in via a prop.
// If neither the prop or the context value for 'window' are present, the DragDropContextProvider
// will just use the global window.
return (
<Frame style={{ width: '100%', height: '100%' }}>
<DragDropContextProvider backend={HTML5Backend}>
<div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Dustbin />
</div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Box name="Glass" />
<Box name="Banana" />
<Box name="Paper" />
</div>
</div>
</DragDropContextProvider>
</Frame>
);
}
}
58 changes: 58 additions & 0 deletions examples/01 Dustbin/Single Target in iframe/__tests__/Box-test.js
@@ -0,0 +1,58 @@
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import expect from 'expect';
import wrapInTestContext from '../../../shared/wrapInTestContext';
import Box from '../Box';

describe('Box', () => {
it('can be tested independently', () => {
// Obtain the reference to the component before React DnD wrapping
const OriginalBox = Box.DecoratedComponent;

// Stub the React DnD connector functions with an identity function
const identity = x => x;

// Render with one set of props and test
let root = TestUtils.renderIntoDocument(
<OriginalBox
name="test"
connectDragSource={identity}
isDragging={false}
/>,
);
let div = TestUtils.findRenderedDOMComponentWithTag(root, 'div');
expect(div.style.opacity).toEqual('1');

// Render with another set of props and test
root = TestUtils.renderIntoDocument(
<OriginalBox
name="test"
connectDragSource={identity}
isDragging
/>,
);
div = TestUtils.findRenderedDOMComponentWithTag(root, 'div');
expect(div.style.opacity).toEqual('0.4');
});

it('can be tested with the testing backend', () => {
// Render with the testing backend
const BoxContext = wrapInTestContext(Box);
const root = TestUtils.renderIntoDocument(<BoxContext name="test" />);

// Obtain a reference to the backend
const backend = root.getManager().getBackend();

// Check that the opacity is 1
let div = TestUtils.findRenderedDOMComponentWithTag(root, 'div');
expect(div.style.opacity).toEqual('1');

// Find the drag source ID and use it to simulate the dragging state
const box = TestUtils.findRenderedComponentWithType(root, Box);
backend.simulateBeginDrag([box.getHandlerId()]);

// Verify that the div changed its opacity
div = TestUtils.findRenderedDOMComponentWithTag(root, 'div');
expect(div.style.opacity).toEqual('0.4');
});
});
25 changes: 25 additions & 0 deletions examples/01 Dustbin/Single Target in iframe/index.js
@@ -0,0 +1,25 @@
import React, { Component } from 'react';
import Container from './Container';

export default class DustbinSingleTargetIframe extends Component {
render() {
return (
<div>
<p>
<b><a href="https://github.com/react-dnd/react-dnd/tree/master/examples/01%20Dustbin/Single%20Target%20in%20iframe">Browse the Source</a></b>
</p>
<p>
This is the same simple example, but nested in an iframe.
</p>
<p>
When you are using the react-dnd-html5-backend, you are limited to drag-and-drop within a single iframe.
</p>
<p>
Using react-dnd inside of an iframe requires a slightly different
container configuration. Check out the source for more details.
</p>
<Container />
</div>
);
}
}
23 changes: 12 additions & 11 deletions examples/01 Dustbin/Single Target/Container.js
@@ -1,23 +1,24 @@
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Dustbin from './Dustbin';
import Box from './Box';

@DragDropContext(HTML5Backend)
export default class Container extends Component {
render() {
return (
<div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Dustbin />
<DragDropContextProvider backend={HTML5Backend}>
<div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Dustbin />
</div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Box name="Glass" />
<Box name="Banana" />
<Box name="Paper" />
</div>
</div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Box name="Glass" />
<Box name="Banana" />
<Box name="Paper" />
</div>
</div>
</DragDropContextProvider>
);
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -94,6 +94,7 @@
"react-dnd-html5-backend": "^2.1.2",
"react-dnd-test-backend": "^1.0.2",
"react-dom": "^15.0.0-rc.2",
"react-frame-component": "^1.0.0",
"react-hot-loader": "^1.2.3",
"request": "^2.79.0",
"rimraf": "^2.5.4",
Expand Down
10 changes: 9 additions & 1 deletion site/Constants.js
Expand Up @@ -47,6 +47,10 @@ export const APIPages = [{
DRAG_DROP_CONTEXT: {
location: 'docs-drag-drop-context.html',
title: 'DragDropContext'
},
DRAG_DROP_CONTEXT_PROVIDER: {
location: 'docs-drag-drop-context-provider.html',
title: 'DragDropContextProvider'
}
}
}, {
Expand Down Expand Up @@ -106,6 +110,10 @@ export const ExamplePages = [{
location: 'examples-dustbin-single-target.html',
title: 'Single Target'
},
DUSTBIN_IFRAME: {
location: 'examples-dustbin-single-target-in-iframe.html',
title: 'Within iframe'
},
DUSTBIN_MULTIPLE_TARGETS: {
location: 'examples-dustbin-multiple-targets.html',
title: 'Multiple Targets'
Expand Down Expand Up @@ -170,4 +178,4 @@ export const ExamplePages = [{
}];

export const DOCS_DEFAULT = APIPages[0].pages.OVERVIEW;
export const EXAMPLES_DEFAULT = ExamplePages[0].pages.CHESSBOARD_TUTORIAL_APP;
export const EXAMPLES_DEFAULT = ExamplePages[0].pages.CHESSBOARD_TUTORIAL_APP;
2 changes: 2 additions & 0 deletions site/IndexPage.js
Expand Up @@ -19,6 +19,7 @@ const APIDocs = {
DROP_TARGET_CONNECTOR: require('../docs/02 Connecting to DOM/DropTargetConnector.md'),
DROP_TARGET_MONITOR: require('../docs/03 Monitoring State/DropTargetMonitor.md'),
DRAG_DROP_CONTEXT: require('../docs/01 Top Level API/DragDropContext.md'),
DRAG_DROP_CONTEXT_PROVIDER: require('../docs/01 Top Level API/DragDropContextProvider.md'),
DRAG_LAYER: require('../docs/01 Top Level API/DragLayer.md'),
DRAG_LAYER_MONITOR: require('../docs/03 Monitoring State/DragLayerMonitor.md'),
HTML5_BACKEND: require('../docs/04 Backends/HTML5.md'),
Expand All @@ -28,6 +29,7 @@ const APIDocs = {
const Examples = {
CHESSBOARD_TUTORIAL_APP: require('../examples/00 Chessboard/Tutorial App').default,
DUSTBIN_SINGLE_TARGET: require('../examples/01 Dustbin/Single Target').default,
DUSTBIN_IFRAME: require('../examples/01 Dustbin/Single Target in iframe').default,
DUSTBIN_MULTIPLE_TARGETS: require('../examples/01 Dustbin/Multiple Targets').default,
DUSTBIN_STRESS_TEST: require('../examples/01 Dustbin/Stress Test').default,
DRAG_AROUND_NAIVE: require('../examples/02 Drag Around/Naive').default,
Expand Down
33 changes: 19 additions & 14 deletions src/DragDropContext.js
Expand Up @@ -4,26 +4,33 @@ import invariant from 'invariant';
import hoistStatics from 'hoist-non-react-statics';
import checkDecoratorArguments from './utils/checkDecoratorArguments';

export default function DragDropContext(backendOrModule) {
checkDecoratorArguments('DragDropContext', 'backend', ...arguments); // eslint-disable-line prefer-rest-params
export const CHILD_CONTEXT_TYPES = {
dragDropManager: PropTypes.object.isRequired,
};

export const createChildContext = (backend, context) => ({
dragDropManager: new DragDropManager(backend, context),
});

export const unpackBackendForEs5Users = (backendOrModule) => {
// Auto-detect ES6 default export for people still using ES5
let backend;
if (typeof backendOrModule === 'object' && typeof backendOrModule.default === 'function') {
backend = backendOrModule.default;
} else {
backend = backendOrModule;
let backend = backendOrModule;
if (typeof backend === 'object' && typeof backend.default === 'function') {
backend = backend.default;
}

invariant(
typeof backend === 'function',
'Expected the backend to be a function or an ES6 module exporting a default function. ' +
'Read more: http://react-dnd.github.io/react-dnd/docs-drag-drop-context.html',
);
return backend;
};

const childContext = {
dragDropManager: new DragDropManager(backend),
};
export default function DragDropContext(backendOrModule) {
checkDecoratorArguments('DragDropContext', 'backend', ...arguments); // eslint-disable-line prefer-rest-params

const backend = unpackBackendForEs5Users(backendOrModule);
const childContext = createChildContext(backend);

return function decorateContext(DecoratedComponent) {
const displayName =
Expand All @@ -36,9 +43,7 @@ export default function DragDropContext(backendOrModule) {

static displayName = `DragDropContext(${displayName})`;

static childContextTypes = {
dragDropManager: PropTypes.object.isRequired,
};
static childContextTypes = CHILD_CONTEXT_TYPES;

getDecoratedComponentInstance() {
invariant(
Expand Down
57 changes: 57 additions & 0 deletions src/DragDropContextProvider.js
@@ -0,0 +1,57 @@
import { PropTypes, Component, Children } from 'react';
import {
CHILD_CONTEXT_TYPES,
createChildContext,
unpackBackendForEs5Users,
} from './DragDropContext';

/**
* This class is a React-Component based version of the DragDropContext.
* This is an alternative to decorating an application component with an ES7 decorator.
*/
export default class DragDropContextProvider extends Component {
static propTypes = {
backend: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
children: PropTypes.element.isRequired,
window: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};

static defaultProps = {
window: undefined,
};

static childContextTypes = CHILD_CONTEXT_TYPES;

static displayName = 'DragDropContextProvider';

static contextTypes = {
window: PropTypes.object,
};

constructor(props, context) {
super(props, context);
this.backend = unpackBackendForEs5Users(props.backend);
}

getChildContext() {
/**
* This property determines which window global to use for creating the DragDropManager.
* If a window has been injected explicitly via props, that is used first. If it is available
* as a context value, then use that, otherwise use the browser global.
*/
const getWindow = () => {
if (this.props && this.props.window) {
return this.props.window;
} else if (this.context && this.context.window) {
return this.context.window;
}
return window;
};

return createChildContext(this.backend, { window: getWindow() });
}

render() {
return Children.only(this.props.children);
}
}

0 comments on commit 1153d82

Please sign in to comment.