From 7779463d9cb1277e067b27ec2fcef76fa79f3ab6 Mon Sep 17 00:00:00 2001 From: Chris Trevino Date: Thu, 21 Mar 2019 12:20:23 -0700 Subject: [PATCH] Hooks Ref'ing Updates (#1280) * docs: clean up native file example * test: add an example case to demonstrate flaw with current hook api * fix: replace refs in useDrag,useDrop with connector functions * feat: work on hooks, perf improvements * refactor: clean up usedrag/usedrop hooks * refactor: clean up the number of hook files * feat: use callback refs in hooks api * feat: memoize drag/drop specs * fix: class api typings * feat: remove the 'preview' option of the drag spec * feat: update previews, documentation * refactor: move isRef * refactor: create SourceConnector, TargetConneector classes * fix: hooks isuses * refactor: make the hooks field constant in the connector classes * fix: reconnect connectors on target change * docs: clean up knight component * fix: add typings to useCollector arguments * fix: tighten typings on dragdrop monitors * fix: some liveness issues, hooks are still wonky atm * refactor: decouple useCollector/useMonitor output from connector types * docs: update usedetachedcomponent docs * fix: correct issues with detached previews, rename the hook to useDetachedPreview * docs: remove previewproperty from dragspec in useDetachedPreview * revert: eliminate implicit memoization. BYOM(emoization of specs) * refactor: clarify the ref mechanism is decorateHandler. Remove recompose since the ref objects will be harmless on FCs * fix: scope down the drag preview wrapper Because of the limitations with drag previews, we can't use arbitrary components, only Image elements. Because of this, we've retooled the DisconnectedDragPreview HOC to be just a DragPreviewImage component which can render disconnected images. * docs: add drag-preview-image docs to sidebar * refactor: replace the portaling mechanism with a renderless component This mechanism for showing drag previews is much, much simpler, but still allows clients to use image previews as components. * docs: add some notes to the new examples --- packages/dnd-core/src/utils/dirtiness.ts | 6 +- .../docs/01 Top Level API/DragPreviewImage.md | 37 ++++ .../docs/05 Hooks-Based API/useDrag.md | 14 +- .../docs/05 Hooks-Based API/useDragPreview.md | 41 ---- .../docs/05 Hooks-Based API/useDrop.md | 13 +- .../customize/handles-and-previews.md | 1 + .../examples/other/drag-source-rerender.md | 11 + packages/documentation/src/constants.ts | 8 + .../src/00 Chessboard/BoardSquare.tsx | 20 +- .../src/00 Chessboard/Knight.tsx | 26 +-- .../src/01 Dustbin/Copy or Move/Box.tsx | 8 +- .../src/01 Dustbin/Copy or Move/Dustbin.tsx | 7 +- .../src/01 Dustbin/Multiple Targets/Box.tsx | 7 +- .../01 Dustbin/Multiple Targets/Dustbin.tsx | 6 +- .../01 Dustbin/Single Target with FCs/Box.tsx | 4 +- .../Single Target with FCs/Dustbin.tsx | 4 +- .../src/01 Dustbin/Single Target/Box.tsx | 10 +- .../src/01 Dustbin/Single Target/Dustbin.tsx | 4 +- .../src/01 Dustbin/Stress Test/Box.tsx | 4 +- .../src/01 Dustbin/Stress Test/Dustbin.tsx | 4 +- .../Custom Drag Layer/Container.tsx | 6 +- .../Custom Drag Layer/DraggableBox.tsx | 14 +- .../src/02 Drag Around/Naive/Box.tsx | 6 +- .../src/02 Drag Around/Naive/Container.tsx | 6 +- .../src/03 Nesting/Drag Sources/SourceBox.tsx | 4 +- .../src/03 Nesting/Drag Sources/TargetBox.tsx | 4 +- .../src/03 Nesting/Drop Targets/Box.tsx | 5 +- .../src/03 Nesting/Drop Targets/Dustbin.tsx | 4 +- .../Cancel on Drop Outside/Card.tsx | 18 +- .../Cancel on Drop Outside/index.tsx | 5 +- .../src/04 Sortable/Simple/Card.tsx | 25 ++- .../src/04 Sortable/Simple/index.tsx | 23 +- .../src/04 Sortable/Stress Test/Card.tsx | 13 +- .../05 Customize/Drop Effects/SourceBox.tsx | 4 +- .../05 Customize/Drop Effects/TargetBox.tsx | 8 +- .../Handles and Previews/BoxWithHandle.tsx | 6 +- .../Handles and Previews/BoxWithImage.tsx | 21 +- .../06 Other/Drag Source Rerender/Example.tsx | 53 +++++ .../06 Other/Drag Source Rerender/index.tsx | 19 ++ .../src/06 Other/Native Files/TargetBox.tsx | 11 +- .../src/06 Other/Native Files/index.tsx | 11 +- packages/examples-hooks/src/index.ts | 4 +- .../examples/src/00 Chessboard/Knight.tsx | 55 +++-- .../06 Other/Drag Source Rerender/Example.tsx | 57 +++++ .../06 Other/Drag Source Rerender/index.tsx | 18 ++ packages/examples/src/index.ts | 2 + packages/react-dnd/package.json | 1 - packages/react-dnd/src/DragDropContext.tsx | 7 +- packages/react-dnd/src/DragLayer.tsx | 7 +- packages/react-dnd/src/DragPreviewImage.tsx | 20 ++ packages/react-dnd/src/DragSource.ts | 4 +- packages/react-dnd/src/DropTarget.ts | 4 +- packages/react-dnd/src/SourceConnector.ts | 199 ++++++++++++++++++ packages/react-dnd/src/TargetConnector.ts | 107 ++++++++++ .../react-dnd/src/createSourceConnector.ts | 125 ----------- packages/react-dnd/src/createSourceFactory.ts | 3 +- .../react-dnd/src/createTargetConnector.ts | 71 ------- packages/react-dnd/src/createTargetFactory.ts | 4 +- packages/react-dnd/src/decorateHandler.tsx | 33 ++- packages/react-dnd/src/hooks/index.ts | 1 - packages/react-dnd/src/hooks/internal/drag.ts | 83 ++++++++ packages/react-dnd/src/hooks/internal/drop.ts | 72 +++++++ .../src/hooks/internal/useCollector.ts | 22 +- .../hooks/internal/useDragSourceMonitor.ts | 78 ------- .../hooks/internal/useDropTargetMonitor.ts | 58 ----- .../src/hooks/internal/useMonitorOutput.ts | 38 ++-- .../src/hooks/internal/useRefObject.ts | 9 + packages/react-dnd/src/hooks/useDrag.ts | 82 +++----- .../react-dnd/src/hooks/useDragPreview.ts | 26 --- packages/react-dnd/src/hooks/useDrop.ts | 61 +++--- packages/react-dnd/src/index.ts | 5 +- packages/react-dnd/src/interfaces/classApi.ts | 13 +- packages/react-dnd/src/interfaces/hooksApi.ts | 20 +- packages/react-dnd/src/interfaces/monitors.ts | 13 +- packages/react-dnd/src/registerSource.ts | 10 +- packages/react-dnd/src/registerTarget.ts | 7 +- .../src/{hooks/util.ts => utils/isRef.ts} | 0 packages/react-dnd/src/wrapConnectorHooks.ts | 10 +- tsconfig.json | 1 + yarn.lock | 28 +-- 80 files changed, 1042 insertions(+), 817 deletions(-) create mode 100644 packages/documentation/markdown/docs/01 Top Level API/DragPreviewImage.md delete mode 100644 packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md create mode 100644 packages/documentation/markdown/examples/other/drag-source-rerender.md create mode 100644 packages/examples-hooks/src/06 Other/Drag Source Rerender/Example.tsx create mode 100644 packages/examples-hooks/src/06 Other/Drag Source Rerender/index.tsx create mode 100644 packages/examples/src/06 Other/Drag Source Rerender/Example.tsx create mode 100644 packages/examples/src/06 Other/Drag Source Rerender/index.tsx create mode 100644 packages/react-dnd/src/DragPreviewImage.tsx create mode 100644 packages/react-dnd/src/SourceConnector.ts create mode 100644 packages/react-dnd/src/TargetConnector.ts delete mode 100644 packages/react-dnd/src/createSourceConnector.ts delete mode 100644 packages/react-dnd/src/createTargetConnector.ts create mode 100644 packages/react-dnd/src/hooks/internal/drag.ts create mode 100644 packages/react-dnd/src/hooks/internal/drop.ts delete mode 100644 packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts delete mode 100644 packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts create mode 100644 packages/react-dnd/src/hooks/internal/useRefObject.ts delete mode 100644 packages/react-dnd/src/hooks/useDragPreview.ts rename packages/react-dnd/src/{hooks/util.ts => utils/isRef.ts} (100%) diff --git a/packages/dnd-core/src/utils/dirtiness.ts b/packages/dnd-core/src/utils/dirtiness.ts index 7a1aa7cb71..e4243d5a21 100644 --- a/packages/dnd-core/src/utils/dirtiness.ts +++ b/packages/dnd-core/src/utils/dirtiness.ts @@ -4,6 +4,9 @@ const intersection = require('lodash/intersection') export const NONE: string[] = [] export const ALL: string[] = [] + // Add these flags for debug +;(NONE as any).__IS_NONE__ = true +;(ALL as any).__IS_ALL__ = true /** * Determines if the given handler IDs are dirty or not. @@ -23,5 +26,6 @@ export function areDirty( return true } - return intersection(handlerIds, dirtyIds).length > 0 + const commonIds = intersection(handlerIds, dirtyIds) + return commonIds.length > 0 } diff --git a/packages/documentation/markdown/docs/01 Top Level API/DragPreviewImage.md b/packages/documentation/markdown/docs/01 Top Level API/DragPreviewImage.md new file mode 100644 index 0000000000..931355c018 --- /dev/null +++ b/packages/documentation/markdown/docs/01 Top Level API/DragPreviewImage.md @@ -0,0 +1,37 @@ +--- +path: '/docs/api/drag-preview-image' +title: 'DragPreviewImage' +--- + +_New to React DnD? [Read the overview](/docs/overview) before jumping into the docs._ + +# DragPreviewImage + +A Component to render an HTML Image element as a disconnected drag preview. + +### Usage + +```js +import HTML5Backend from 'react-dnd-html5-backend' +import { DragSource, DragPreviewImage } from 'react-dnd' + +function DraggableHouse({ connectDragSource, connectDragPreview }) { + return ( + <> + +
🏠
+ + ) +} +export default DragSource( + /* ... */ + (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + }), +) +``` + +### Props + +- **`connect`**: Required. The drag preview connector function diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md index 5ddb03e569..348ae758d9 100644 --- a/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md +++ b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrag.md @@ -16,21 +16,23 @@ import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } const { useDrag } = dnd function DraggableComponent(props) { - const [collectedProps, ref] = useDrag({ + const [collectedProps, drag] = useDrag({ item: { id, type }, }) - return
...
+ return
...
} ``` #### Parameters -- **`spec`** Specification object, see below for details on how to construct this +- **`spec`** A specification object, see below for details on how to construct this +- **`memoization parameters`** - values to use when rebuilding the memoized specification #### Return Value Array - **`Index 0`**: An object containing collected properties from the collect function. If no `collect` function is defined, an empty object is returned. -- **`Index 1`**: The React ref to use. This is automatically created if no `ref` field is defined on the specification object. The ref must be attached to the draggable portion of the DOM. +- **`Index 1`**: A connector function for the drag source. This must be attached to the draggable portion of the DOM. +- **`Index 2`**: A connector function for the drag preview. This may be attached to the preview portion of the DOM. ### Specification Object Members @@ -38,10 +40,6 @@ function DraggableComponent(props) { `item.type` **must be set**, and it must be either a string, an ES6 symbol`. Only the [drop targets](/docs/api/drop-target) registered for the same type will react to this item. Read the [overview](/docs/overview) to learn more about the items and types. -- **`ref`**: Optional. A ref object to use to attach to the draggable element. If this is unset, one will be created ad returned. - -- **`preview`**: Optional. An HTML Element or a ref object attached to the dragPreview element. Consider using the `useDragPreview` hook to create this for you. - - **`previewOptions`**: Optional. A plain JavaScript object describing drag preview options. * **`options`**: Optional. A plain object. If some of the props to your component are not scalar (that is, are not primitive values or functions), specifying a custom`arePropsEqual(props, otherProps)`function inside the`options` object can improve the performance. Unless you have performance problems, don't worry about it. diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md deleted file mode 100644 index 8841418a16..0000000000 --- a/packages/documentation/markdown/docs/05 Hooks-Based API/useDragPreview.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -path: '/docs/api/use-drag-preview' -title: 'useDragPreview' ---- - -## EXPERIMENTAL API - UNSTABLE - -_New to React DnD? [Read the overview](/docs/overview) before jumping into the docs._ - -# useDragPreview - -A hook to use the current component as a drag-layer. - -```js -import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } from 'react-dnd' -const { useDragPreview } = dnd - -function DragLayerPreview(props) { - const [DragPreview, preview] = useDragPreview(spec) - const [collectedProps, ref] = useDrag({ - item: { id, type }, - preview, - }) - - return ( - <> - -
...drag item...
- - ) -} -``` - -#### Parameters - -- **`dragPreview`** A refForwarding component that will render the drag preview. - -#### Return Value Array - -- **`Index 0`**: A component to render the dragPreview in your render method. -- **`Index 1`**: The drag preview ref object. This should be passed into useDrag's specification diff --git a/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md index 6cf2879250..3816047194 100644 --- a/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md +++ b/packages/documentation/markdown/docs/05 Hooks-Based API/useDrop.md @@ -16,27 +16,28 @@ import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } const { useDrop } = dnd function myDropTarget(props) { - const [collectedProps, ref] = useDrop({ accept }) + const [collectedProps, drop] = useDrop({ + accept, + }) - return
Drop Target
+ return
Drop Target
} ``` #### Parameters -- **`spec`** Specification object, see below for details on how to construct this +- **`spec`** A specification object, see below for details on how to construct this +- **`memoization parameters`** - values to use when rebuilding the memoized specification #### Return Value Array - **`Index 0`**: An object containing collected properties from the collect function. If no `collect` function is defined, an empty object is returned. -- **`Index 1`**: The React ref to use. This is automatically created if no `ref` field is defined on the specification object. The ref must be attached to the droppable area of the DOM. +- **`Index 1`**: A connector function for the drop target. This must be attached to the drop-target portion of the DOM. ### Specification Object Members - **`accept`**: Required. A string, an ES6 symbol, an array of either, or a function that returns either of those, given component's `props`. This drop target will only react to the items produced by the [drag sources](/docs/api/drag-source) of the specified type or types. Read the [overview](/docs/overview) to learn more about the items and types. -* **`ref`**: Optional. A ref object to use to attach to the draggable element. If this is unset, one will be created ad returned. - * **`options`**: Optional. A plain object. If some of the props to your component are not scalar (that is, are not primitive values or functions), specifying a custom `arePropsEqual(props, otherProps)` function inside the `options` object can improve the performance. Unless you have performance problems, don't worry about it. * **`drop(item, monitor)`**: Optional. Called when a compatible item is dropped on the target. You may either return undefined, or a plain object. If you return an object, it is going to become _the drop result_ and will be available to the drag source in its `endDrag` method as `monitor.getDropResult()`. This is useful in case you want to perform different actions depending on which target received the drop. If you have nested drop targets, you can test whether a nested target has already handled `drop` by checking `monitor.didDrop()` and `monitor.getDropResult()`. Both this method and the source's `endDrag` method are good places to fire Flux actions. This method will not be called if `canDrop()` is defined and returns `false`. diff --git a/packages/documentation/markdown/examples/customize/handles-and-previews.md b/packages/documentation/markdown/examples/customize/handles-and-previews.md index 873e543779..e13e8445c5 100644 --- a/packages/documentation/markdown/examples/customize/handles-and-previews.md +++ b/packages/documentation/markdown/examples/customize/handles-and-previews.md @@ -8,6 +8,7 @@ title: 'Handles and Previews' React DnD lets you choose the draggable node, as well as the drag preview node in your component's `render` function. + You may also use an [Image](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image) instance that you created programmatically once it has loaded. diff --git a/packages/documentation/markdown/examples/other/drag-source-rerender.md b/packages/documentation/markdown/examples/other/drag-source-rerender.md new file mode 100644 index 0000000000..2271fb8c56 --- /dev/null +++ b/packages/documentation/markdown/examples/other/drag-source-rerender.md @@ -0,0 +1,11 @@ +--- +path: '/examples/other/drag-source-rerender' +title: 'Drag Source Rerender' +--- + +[JavaScript](https://github.com/react-dnd/react-dnd/tree/gh-pages/examples_js/06%20Other/Drag%20Source%20Rerender) +[TypeScript](https://github.com/react-dnd/react-dnd/tree/master/packages/examples/src/06%20Other/Drag%20Source%20Rerender) + +Regression example using a drop target that's connected as a child + + diff --git a/packages/documentation/src/constants.ts b/packages/documentation/src/constants.ts index 16235d2a2d..8da314fb67 100644 --- a/packages/documentation/src/constants.ts +++ b/packages/documentation/src/constants.ts @@ -60,6 +60,10 @@ export const APIPages: PageGroup[] = [ location: '/docs/api/drag-layer', title: 'DragLayer', }, + DRAG_PREVIEW_IMAGE: { + location: '/docs/api/drag-preview-image', + title: 'DragPreviewImage', + }, DRAG_DROP_CONTEXT: { location: '/docs/api/drag-drop-context', title: 'DragDropContext', @@ -242,6 +246,10 @@ export const ExamplePages: PageGroup[] = [ { title: 'Other Cases', pages: { + OTHER_DRAG_SOURCE_RERENDER: { + location: '/examples/other/drag-source-rerender', + title: 'Drag Source Rerender', + }, OTHER_NATIVE_FILES: { location: '/examples/other/native-files', title: 'Native Files', diff --git a/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx b/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx index e7a9b6a061..c5e1914207 100644 --- a/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx +++ b/packages/examples-hooks/src/00 Chessboard/BoardSquare.tsx @@ -14,30 +14,32 @@ export interface BoardSquareProps { children: any } -export const BoardSquare: React.FC = ( - props: BoardSquareProps, -) => { - const [{ isOver, canDrop }, ref] = useDrop({ +export const BoardSquare: React.FC = ({ + x, + y, + children, +}: BoardSquareProps) => { + const [{ isOver, canDrop }, drop] = useDrop({ accept: ItemTypes.KNIGHT, - canDrop: () => canMoveKnight(props.x, props.y), - drop: () => moveKnight(props.x, props.y), + canDrop: () => canMoveKnight(x, y), + drop: () => moveKnight(x, y), collect: mon => ({ isOver: !!mon.isOver(), canDrop: !!mon.canDrop(), }), }) - const black = (props.x + props.y) % 2 === 1 + const black = (x + y) % 2 === 1 return (
- {props.children} + {children} {isOver && !canDrop && } {!isOver && canDrop && } {isOver && canDrop && } diff --git a/packages/examples-hooks/src/00 Chessboard/Knight.tsx b/packages/examples-hooks/src/00 Chessboard/Knight.tsx index bf3bcbdb17..8a3cfe9f5b 100644 --- a/packages/examples-hooks/src/00 Chessboard/Knight.tsx +++ b/packages/examples-hooks/src/00 Chessboard/Knight.tsx @@ -1,11 +1,13 @@ import * as React from 'react' -import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' +import { + DragPreviewImage, + __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__, +} from 'react-dnd' import ItemTypes from './ItemTypes' import knightImage from './knightImage' const { useDrag, - useDragPreview, } = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ const knightStyle: React.CSSProperties = { @@ -14,21 +16,9 @@ const knightStyle: React.CSSProperties = { cursor: 'move', } -const KnightDragPreview = React.forwardRef( - (props, ref: React.Ref) => { - if (typeof Image === 'undefined') { - return null - } - return - }, -) - export const Knight: React.FC = () => { - const item = { type: ItemTypes.KNIGHT } - const [DragPreview, preview] = useDragPreview(KnightDragPreview) - const [{ isDragging }, ref] = useDrag({ - item, - preview, + const [{ isDragging }, drag, preview] = useDrag({ + item: { type: ItemTypes.KNIGHT }, collect: mon => ({ isDragging: !!mon.isDragging(), }), @@ -36,9 +26,9 @@ export const Knight: React.FC = () => { return ( <> - +
= ({ name }) => { const item = { name, type: ItemTypes.BOX } - const [{ opacity }, ref] = useDrag({ + const [{ opacity }, drag] = useDrag({ item, end(dropResult?: DropResult) { if (dropResult) { @@ -50,13 +50,13 @@ const Box: React.FC = ({ name }) => { alert(alertMessage) } }, - collect: monitor => ({ + collect: (monitor: any) => ({ opacity: monitor.isDragging() ? 0.4 : 1, }), }) return ( -
+
{name}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx index 33af69c753..b5f686da90 100644 --- a/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Copy or Move/Dustbin.tsx @@ -33,21 +33,22 @@ function selectBackgroundColor(isActive: boolean, canDrop: boolean) { } const Dustbin: React.FC = ({ allowedDropEffect }) => { - const [{ canDrop, isOver }, ref] = useDrop({ + const [{ canDrop, isOver }, drop] = useDrop({ accept: ItemTypes.BOX, drop: () => ({ name: `${allowedDropEffect} Dustbin`, allowedDropEffect, }), - collect: monitor => ({ + collect: (monitor: any) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }) + const isActive = canDrop && isOver const backgroundColor = selectBackgroundColor(isActive, canDrop) return ( -
+
{`Works with ${allowedDropEffect} drop effect`}

diff --git a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx index 048049d4ef..2adb99b9db 100644 --- a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Box.tsx @@ -22,16 +22,15 @@ export interface BoxProps { } const Box: React.FC = ({ name, type, isDropped }) => { - const item = { name, type } - const [{ opacity }, ref] = useDrag({ - item, + const [{ opacity }, drag] = useDrag({ + item: { name, type }, collect: monitor => ({ opacity: monitor.isDragging() ? 0.4 : 1, }), }) return ( -
+
{isDropped ? {name} : name}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx index 3c5fe1df06..ac7f0f9107 100644 --- a/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Multiple Targets/Dustbin.tsx @@ -28,9 +28,9 @@ const Dustbin: React.FC = ({ lastDroppedItem, onDrop, }) => { - const [{ isOver, canDrop }, ref] = useDrop({ + const [{ isOver, canDrop }, drop] = useDrop({ accept, - drop: item => onDrop(item), + drop: onDrop, collect: monitor => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), @@ -46,7 +46,7 @@ const Dustbin: React.FC = ({ } return ( -
+
{isActive ? 'Release to drop' : `This dustbin accepts: ${accept.join(', ')}`} diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx index e40fd7aef2..490ec8e408 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Box.tsx @@ -24,7 +24,7 @@ export interface BoxProps { const Box: React.FC = ({ name }) => { const item = { name, type: ItemTypes.BOX } - const [{ opacity }, ref] = useDrag({ + const [{ opacity }, drag] = useDrag({ item, end: (dropResult?: { name: string }) => { if (dropResult) { @@ -37,7 +37,7 @@ const Box: React.FC = ({ name }) => { }) return ( -
+
{name}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx index 531656d320..6def428ec6 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target with FCs/Dustbin.tsx @@ -19,7 +19,7 @@ const style: React.CSSProperties = { } const Dustbin: React.FC = () => { - const [{ isOver, canDrop }, ref] = useDrop({ + const [{ isOver, canDrop }, drop] = useDrop({ accept: ItemTypes.BOX, drop: () => ({ name: 'Dustbin' }), collect: monitor => ({ @@ -37,7 +37,7 @@ const Dustbin: React.FC = () => { } return ( -
+
{isActive ? 'Release to drop' : 'Drag a box here'}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx index ac0eaf583a..122e62ee66 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target/Box.tsx @@ -20,13 +20,11 @@ interface BoxProps { } const Box: React.FC = ({ name }) => { - const item = { name, type: ItemTypes.BOX } - - const [{ isDragging }, ref] = useDrag({ - item, + const [{ isDragging }, drag] = useDrag({ + item: { name, type: ItemTypes.BOX }, end: (dropResult?: { name: string }) => { if (dropResult) { - alert(`You dropped ${item.name} into ${dropResult.name}!`) + alert(`You dropped ${name} into ${dropResult.name}!`) } }, collect: monitor => ({ @@ -36,7 +34,7 @@ const Box: React.FC = ({ name }) => { const opacity = isDragging ? 0.4 : 1 return ( -
+
{name}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx index 938208967c..98465ac542 100644 --- a/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Single Target/Dustbin.tsx @@ -20,7 +20,7 @@ const style: React.CSSProperties = { } const Dustbin: React.FC = () => { - const [{ canDrop, isOver }, ref] = useDrop({ + const [{ canDrop, isOver }, drop] = useDrop({ accept: ItemTypes.BOX, drop: () => ({ name: 'Dustbin' }), collect: monitor => ({ @@ -38,7 +38,7 @@ const Dustbin: React.FC = () => { } return ( -
+
{isActive ? 'Release to drop' : 'Drag a box here'}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx b/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx index 6e8fcb676f..4c0a46ffe3 100644 --- a/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Stress Test/Box.tsx @@ -22,7 +22,7 @@ export interface BoxProps { } const Box: React.FC = ({ name, type, isDropped }) => { - const [{ isDragging }, ref] = useDrag({ + const [{ isDragging }, drag] = useDrag({ item: { name, type }, isDragging(monitor) { const item = monitor.getItem() @@ -36,7 +36,7 @@ const Box: React.FC = ({ name, type, isDropped }) => { const opacity = isDragging ? 0.4 : 1 return ( -
+
{isDropped ? {name} : name}
) diff --git a/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx b/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx index 29effded52..dd80452efc 100644 --- a/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx +++ b/packages/examples-hooks/src/01 Dustbin/Stress Test/Dustbin.tsx @@ -29,7 +29,7 @@ const Dustbin: React.FC = ({ accepts: accept, onDrop, }) => { - const [{ isOver, canDrop }, ref] = useDrop({ + const [{ isOver, canDrop }, drop] = useDrop({ accept, collect: monitor => ({ isOver: monitor.isOver(), @@ -47,7 +47,7 @@ const Dustbin: React.FC = ({ } return ( -
+
{isActive ? 'Release to drop' : `This dustbin accepts: ${accept.join(', ')}`} diff --git a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx index f96920572e..352d4b975a 100644 --- a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/Container.tsx @@ -44,9 +44,7 @@ const Container: React.FC = props => { ) } - const ref = React.useRef(null) - useDrop({ - ref, + const [, drop] = useDrop({ accept: ItemTypes.BOX, drop(item: DragItem, monitor) { const delta = monitor.getDifferenceFromInitialOffset() as { @@ -66,7 +64,7 @@ const Container: React.FC = props => { }) return ( -
+
{Object.keys(boxes).map(key => renderBox(boxes[key], key))}
) diff --git a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx index 26375d2544..a6a0dd0631 100644 --- a/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Custom Drag Layer/DraggableBox.tsx @@ -11,7 +11,8 @@ const { } = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ function getStyles( - { left, top }: DraggableBoxProps, + left: number, + top: number, isDragging: boolean, ): React.CSSProperties { const transform = `translate3d(${left}px, ${top}px, 0)` @@ -35,12 +36,8 @@ export interface DraggableBoxProps { const DraggableBox: React.FC = props => { const { id, title, left, top } = props - const item = { type: ItemTypes.BOX, id, title, left, top } - const [{ isDragging }, ref] = useDrag({ - item, - // Use empty image as a drag preview so browsers don't draw it - // and we can draw whatever we want on the custom drag layer instead. - preview: getEmptyImage(), + const [{ isDragging }, drag, preview] = useDrag({ + item: { type: ItemTypes.BOX, id }, previewOptions: { // IE fallback: specify that we'd rather screenshot the node // when it already knows it's being dragged so we can hide it with CSS. @@ -51,8 +48,9 @@ const DraggableBox: React.FC = props => { }), }) + preview(getEmptyImage()) return ( -
+
) diff --git a/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx b/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx index 87cbfebef6..81fb2a1741 100644 --- a/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Naive/Box.tsx @@ -28,7 +28,7 @@ const Box: React.FC = ({ hideSourceOnDrag, children, }) => { - const [{ isDragging }, ref] = useDrag({ + const [{ isDragging }, drag] = useDrag({ item: { id, left, top, type: ItemTypes.BOX }, collect: monitor => ({ isDragging: monitor.isDragging(), @@ -36,10 +36,10 @@ const Box: React.FC = ({ }) if (isDragging && hideSourceOnDrag) { - return
+ return
} return ( -
+
{children}
) diff --git a/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx b/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx index 89b8a08fae..cd70e1da7d 100644 --- a/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx +++ b/packages/examples-hooks/src/02 Drag Around/Naive/Container.tsx @@ -39,9 +39,7 @@ const Container: React.FC = ({ hideSourceOnDrag }) => { b: { top: 180, left: 20, title: 'Drag me too' }, }) - const ref = React.useRef(null) - useDrop({ - ref, + const [, drop] = useDrop({ accept: ItemTypes.BOX, drop(item: DragItem, monitor) { const delta = monitor.getDifferenceFromInitialOffset() as XYCoord @@ -63,7 +61,7 @@ const Container: React.FC = ({ hideSourceOnDrag }) => { } return ( -
+
{Object.keys(boxes).map(key => { const { left, top, title } = boxes[key] return ( diff --git a/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx b/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx index a8bb3ea39f..0beef5080f 100644 --- a/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drag Sources/SourceBox.tsx @@ -24,7 +24,7 @@ const SourceBox: React.FC = ({ onToggleForbidDrag, children, }) => { - const [{ isDragging }, ref] = useDrag({ + const [{ isDragging }, drag] = useDrag({ item: { type: `${color}` }, canDrag: () => !forbidDrag, collect: monitor => ({ @@ -47,7 +47,7 @@ const SourceBox: React.FC = ({ return (
= ({ onDrop, lastDroppedColor }) => { - const [{ isOver, draggingColor, canDrop }, ref] = useDrop({ + const [{ isOver, draggingColor, canDrop }, drop] = useDrop({ accept: [Colors.YELLOW, Colors.BLUE], drop(item: DragItem) { onDrop(item.type) @@ -49,7 +49,7 @@ const TargetBox: React.FC = ({ onDrop, lastDroppedColor }) => { } return ( -
+

Drop here.

{!canDrop && lastDroppedColor &&

Last dropped: {lastDroppedColor}

} diff --git a/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx b/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx index f8aa094ce3..13a05dae2f 100644 --- a/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drop Targets/Box.tsx @@ -14,10 +14,9 @@ const style = { } const Box: React.FC = () => { - const ref = React.useRef(null) - useDrag({ ref, item: { type: ItemTypes.BOX } }) + const [, drag] = useDrag({ item: { type: ItemTypes.BOX } }) return ( -
+
Drag me
) diff --git a/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx b/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx index 49e753616b..5eb7e14770 100644 --- a/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx +++ b/packages/examples-hooks/src/03 Nesting/Drop Targets/Dustbin.tsx @@ -34,7 +34,7 @@ const Dustbin: React.FC = ({ greedy, children }) => { const [hasDropped, setHasDropped] = React.useState(false) const [hasDroppedOnChild, setHasDroppedOnChild] = React.useState(false) - const [{ isOver, isOverCurrent }, ref] = useDrop({ + const [{ isOver, isOverCurrent }, drop] = useDrop({ accept: ItemTypes.BOX, drop(item, monitor) { const didDrop = monitor.didDrop() @@ -58,7 +58,7 @@ const Dustbin: React.FC = ({ greedy, children }) => { } return ( -
+
{text}
{hasDropped && dropped {hasDroppedOnChild && ' on child'}} diff --git a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx index 465de25aab..0906237e5d 100644 --- a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx +++ b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/Card.tsx @@ -21,19 +21,25 @@ export interface CardProps { findCard: (id: string) => { index: number } } +interface Item { + type: string + id: string + originalIndex: string +} + const Card: React.FC = ({ id, text, moveCard, findCard }) => { - const [{ isDragging }, ref] = useDrag({ - item: { type: ItemTypes.CARD, id, originalIndex: findCard(id).index }, + const originalIndex = findCard(id).index + const [{ isDragging }, drag] = useDrag({ + item: { type: ItemTypes.CARD, id, originalIndex }, collect: monitor => ({ isDragging: monitor.isDragging(), }), }) - useDrop({ - ref, + const [, drop] = useDrop({ accept: ItemTypes.CARD, canDrop: () => false, - hover({ id: draggedId }: { id: string }) { + hover({ id: draggedId }: Item) { if (draggedId !== id) { const { index: overIndex } = findCard(id) moveCard(draggedId, overIndex) @@ -43,7 +49,7 @@ const Card: React.FC = ({ id, text, moveCard, findCard }) => { const opacity = isDragging ? 0 : 1 return ( -
+
drag(drop(node))} style={{ ...style, opacity }}> {text}
) diff --git a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx index 89e3274bc7..d492e205df 100644 --- a/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx +++ b/packages/examples-hooks/src/04 Sortable/Cancel on Drop Outside/index.tsx @@ -66,12 +66,11 @@ const Container: React.FC = () => { } } - const ref = React.useRef(null) - useDrop({ ref, accept: ItemTypes.CARD }) + const [, drop] = useDrop({ accept: ItemTypes.CARD }) return ( <>

EXPERIMENTAL API

-
+
{cards.map(card => ( void } +interface DragItem { + index: number + id: string + type: string +} const Card: React.FC = ({ id, text, index, moveCard }) => { - const ref = React.useRef(null) - useDrop({ - ref, + const ref = useRef(null) + const [, drop] = useDrop({ accept: ItemTypes.CARD, - hover(item: { index: number }, monitor) { + hover(item: DragItem, monitor: DropTargetMonitor) { if (!ref.current) { return } @@ -78,15 +85,15 @@ const Card: React.FC = ({ id, text, index, moveCard }) => { }, }) - const [{ isDragging }] = useDrag({ - ref, + const [{ isDragging }, drag] = useDrag({ item: { type: ItemTypes.CARD, id, index }, - collect: monitor => ({ + collect: (monitor: any) => ({ isDragging: monitor.isDragging(), }), }) const opacity = isDragging ? 0 : 1 + drag(drop(ref)) return (
{text} diff --git a/packages/examples-hooks/src/04 Sortable/Simple/index.tsx b/packages/examples-hooks/src/04 Sortable/Simple/index.tsx index 32c8c74f44..45efb0c885 100644 --- a/packages/examples-hooks/src/04 Sortable/Simple/index.tsx +++ b/packages/examples-hooks/src/04 Sortable/Simple/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useState, useCallback } from 'react' import Card from './Card' import update from 'immutability-helper' import { ContainerProps } from '../../02 Drag Around/Naive/Container' @@ -16,7 +16,7 @@ export interface ContainerState { const Container: React.FC = ({}) => { { - const [cards, setCards] = React.useState([ + const [cards, setCards] = useState([ { id: 1, text: 'Write a cool JS library', @@ -48,14 +48,17 @@ const Container: React.FC = ({}) => { }, ]) - const moveCard = (dragIndex: number, hoverIndex: number) => { - const dragCard = cards[dragIndex] - setCards( - update(cards, { - $splice: [[dragIndex, 1], [hoverIndex, 0, dragCard]], - }), - ) - } + const moveCard = useCallback( + (dragIndex: number, hoverIndex: number) => { + const dragCard = cards[dragIndex] + setCards( + update(cards, { + $splice: [[dragIndex, 1], [hoverIndex, 0, dragCard]], + }), + ) + }, + [cards], + ) const renderCard = (card: { id: number; text: string }, index: number) => { return ( diff --git a/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx b/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx index 4cd2242895..26c067989d 100644 --- a/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx +++ b/packages/examples-hooks/src/04 Sortable/Stress Test/Card.tsx @@ -21,26 +21,27 @@ export interface CardProps { } const Card: React.FC = ({ id, text, moveCard }) => { - const [{ isDragging }, ref] = useDrag({ + const ref = React.useRef(null) + const [{ isDragging }, connectDrag] = useDrag({ item: { id, type: ItemTypes.CARD }, - collect: monitor => ({ + collect: (monitor: any) => ({ isDragging: monitor.isDragging(), }), }) - useDrop({ - ref, + const [, connectDrop] = useDrop({ accept: ItemTypes.CARD, - hover({ id: draggedId }: { id: string }) { + hover({ id: draggedId }: { id: string; type: string }) { if (draggedId !== id) { moveCard(draggedId, id) } }, }) + connectDrag(ref) + connectDrop(ref) const opacity = isDragging ? 0 : 1 const containerStyle = React.useMemo(() => ({ ...style, opacity }), [opacity]) - return (
{text} diff --git a/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx b/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx index ceabdab30a..fe8e0d6c8f 100644 --- a/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx +++ b/packages/examples-hooks/src/05 Customize/Drop Effects/SourceBox.tsx @@ -20,7 +20,7 @@ export interface SourceBoxProps { } const SourceBox: React.FC = ({ showCopyIcon }) => { - const [{ opacity }, ref] = useDrag({ + const [{ opacity }, drag] = useDrag({ item: { type: ItemTypes.BOX }, options: { dropEffect: showCopyIcon ? 'copy' : 'move', @@ -31,7 +31,7 @@ const SourceBox: React.FC = ({ showCopyIcon }) => { }) return ( -
+
When I am over a drop zone, I have {showCopyIcon ? 'copy' : 'no'} icon.
) diff --git a/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx b/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx index bc7209d77f..c6de2b07b3 100644 --- a/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx +++ b/packages/examples-hooks/src/05 Customize/Drop Effects/TargetBox.tsx @@ -15,17 +15,15 @@ const style: React.CSSProperties = { } const TargetBox: React.FC = () => { - const [{ canDrop, isOver }, ref] = useDrop({ + const [{ isActive }, drop] = useDrop({ accept: ItemTypes.BOX, collect: monitor => ({ - canDrop: monitor.canDrop(), - isOver: monitor.isOver(), + isActive: monitor.canDrop() && monitor.isOver(), }), }) - const isActive = canDrop && isOver return ( -
+
{isActive ? 'Release to drop' : 'Drag item here'}
) diff --git a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx index 10365615ef..02bbf27ca8 100644 --- a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx +++ b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithHandle.tsx @@ -23,10 +23,8 @@ const handleStyle: React.CSSProperties = { } const BoxWithHandle: React.FC = () => { - const preview = React.useRef(null) - const [{ opacity }, ref] = useDrag({ + const [{ opacity }, drag, preview] = useDrag({ item: { type: ItemTypes.BOX }, - preview, collect: monitor => ({ opacity: monitor.isDragging() ? 0.4 : 1, }), @@ -34,7 +32,7 @@ const BoxWithHandle: React.FC = () => { return (
-
+
Drag me by the handle
) diff --git a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx index be8ebb9834..dd3295b880 100644 --- a/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx +++ b/packages/examples-hooks/src/05 Customize/Handles and Previews/BoxWithImage.tsx @@ -1,11 +1,13 @@ import * as React from 'react' -import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ } from 'react-dnd' +import { + DragPreviewImage, + __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__, +} from 'react-dnd' import ItemTypes from './ItemTypes' import boxImage from './boxImage' const { useDrag, - useDragPreview, } = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ const style = { @@ -17,18 +19,9 @@ const style = { width: '20rem', } -const BoxImage = React.forwardRef((props, ref: React.Ref) => { - if (typeof Image === 'undefined') { - return null - } - return -}) - const BoxWithImage: React.FC = () => { - const [DragPreview, preview] = useDragPreview(BoxImage) - const [{ opacity }, ref] = useDrag({ + const [{ opacity }, drag, preview] = useDrag({ item: { type: ItemTypes.BOX }, - preview, collect: monitor => ({ opacity: monitor.isDragging() ? 0.4 : 1, }), @@ -36,8 +29,8 @@ const BoxWithImage: React.FC = () => { return ( <> - -
+ +
Drag me to see an image
diff --git a/packages/examples-hooks/src/06 Other/Drag Source Rerender/Example.tsx b/packages/examples-hooks/src/06 Other/Drag Source Rerender/Example.tsx new file mode 100644 index 0000000000..091966d5ae --- /dev/null +++ b/packages/examples-hooks/src/06 Other/Drag Source Rerender/Example.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { + __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__, + ConnectDragSource, +} from 'react-dnd' + +const { + useDrag, +} = __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ + +const Parent: React.FC = () => { + const [{ isDragging }, drag] = useDrag({ + item: { type: 'KNIGHT' }, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + }) + + return {isDragging ? 'Dragging' : 'Drag me'} +} + +export default Parent + +interface ChildProps { + drag: ConnectDragSource +} +const Child: React.FC = ({ drag, children }) => { + const [open, setOpen] = React.useState(true) + const toggle = React.useCallback(() => setOpen(!open), [open]) + + return ( +
+ + {open ? ( +
+ {children} +
+ ) : null} +
+ ) +} diff --git a/packages/examples-hooks/src/06 Other/Drag Source Rerender/index.tsx b/packages/examples-hooks/src/06 Other/Drag Source Rerender/index.tsx new file mode 100644 index 0000000000..63c82dd19b --- /dev/null +++ b/packages/examples-hooks/src/06 Other/Drag Source Rerender/index.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' +import Example from './Example' + +const Container: React.FC = () => { + return ( + <> +

EXPERIMENTAL API

+

+ Drag the box before hiding then hide it and show it again and try again. +

+
+ Note: this is more of a test-case then a usage demo. It will go away in + the future. +
+ + + ) +} +export default Container diff --git a/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx b/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx index 2662f524e9..ebcc914dd8 100644 --- a/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx +++ b/packages/examples-hooks/src/06 Other/Native Files/TargetBox.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { NativeTypes } from 'react-dnd-html5-backend' import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__, DropTargetMonitor, @@ -16,14 +17,13 @@ const style: React.CSSProperties = { } export interface TargetBoxProps { - accepts: string[] onDrop: (props: TargetBoxProps, monitor: DropTargetMonitor) => void } const TargetBox: React.FC = props => { - const { accepts: accept, onDrop } = props - const [{ canDrop, isOver }, ref] = useDrop({ - accept, + const { onDrop } = props + const [{ canDrop, isOver }, drop] = useDrop({ + accept: [NativeTypes.FILE], drop(item, monitor) { if (onDrop) { onDrop(props, monitor) @@ -34,9 +34,10 @@ const TargetBox: React.FC = props => { canDrop: monitor.canDrop, }), }) + const isActive = canDrop && isOver return ( -
+
{isActive ? 'Release to drop' : 'Drag file here'}
) diff --git a/packages/examples-hooks/src/06 Other/Native Files/index.tsx b/packages/examples-hooks/src/06 Other/Native Files/index.tsx index 3639b23379..c6b74f00b5 100644 --- a/packages/examples-hooks/src/06 Other/Native Files/index.tsx +++ b/packages/examples-hooks/src/06 Other/Native Files/index.tsx @@ -1,14 +1,11 @@ -import React, { useState, useCallback } from 'react' +import * as React from 'react' +import { useState, useCallback } from 'react' import { DropTargetMonitor } from 'react-dnd' -import { NativeTypes } from 'react-dnd-html5-backend' import TargetBox from './TargetBox' import FileList from './FileList' -export interface ContainerState { - droppedFiles: any[] -} + const Container: React.FC = () => { const [droppedFiles, setDroppedFiles] = useState([]) - const { FILE } = NativeTypes const handleFileDrop = useCallback( (item: any, monitor: DropTargetMonitor) => { @@ -23,7 +20,7 @@ const Container: React.FC = () => { return ( <>

EXPERIMENTAL API

- + ) diff --git a/packages/examples-hooks/src/index.ts b/packages/examples-hooks/src/index.ts index 76e813be7a..2812532262 100644 --- a/packages/examples-hooks/src/index.ts +++ b/packages/examples-hooks/src/index.ts @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import chessboard from './00 Chessboard' import dustbinCopyOrMove from './01 Dustbin/Copy or Move' import dustbinMultipleTargets from './01 Dustbin/Multiple Targets' @@ -15,6 +15,7 @@ import sortableSimple from './04 Sortable/Simple' import sortableStressTest from './04 Sortable/Stress Test' import customizeDropEffects from './05 Customize/Drop Effects' import customizeHandlesAndPreviews from './05 Customize/Handles and Previews' +import dragSourceRerender from './06 Other/Drag Source Rerender' import otherNativeFiles from './06 Other/Native Files' export * from './isDebugMode' @@ -37,5 +38,6 @@ export const componentIndex: { 'sortable-stress-test': sortableStressTest, 'customize-drop-effects': customizeDropEffects, 'customize-handles-and-previews': customizeHandlesAndPreviews, + 'other-drag-source-rerender': dragSourceRerender, 'other-native-files': otherNativeFiles, } diff --git a/packages/examples/src/00 Chessboard/Knight.tsx b/packages/examples/src/00 Chessboard/Knight.tsx index 74bb333ae7..2d66f91295 100644 --- a/packages/examples/src/00 Chessboard/Knight.tsx +++ b/packages/examples/src/00 Chessboard/Knight.tsx @@ -5,59 +5,52 @@ import { ConnectDragPreview, DragSourceConnector, DragSourceMonitor, - DragSourceCollector, + DragPreviewImage, } from 'react-dnd' import ItemTypes from './ItemTypes' import knightImage from './knightImage' -const knightSource = { - beginDrag() { - return {} - }, -} - const knightStyle: React.CSSProperties = { fontSize: 40, fontWeight: 'bold', cursor: 'move', } -const collect: DragSourceCollector = ( - connect: DragSourceConnector, - monitor: DragSourceMonitor, -) => ({ - connectDragSource: connect.dragSource(), - connectDragPreview: connect.dragPreview(), - isDragging: monitor.isDragging(), -}) - export interface KnightProps { connectDragSource: ConnectDragSource connectDragPreview: ConnectDragPreview isDragging?: boolean } -class Knight extends React.Component { - public componentDidMount() { - const img = new Image() - img.src = knightImage - img.onload = () => - this.props.connectDragPreview && this.props.connectDragPreview(img) - } - - public render() { - const { connectDragSource, isDragging } = this.props - return connectDragSource( +const Knight: React.FC = ({ + connectDragSource, + connectDragPreview, + isDragging, +}) => { + return ( + <> +
♘ -
, - ) - } +
+ + ) } -export default DragSource(ItemTypes.KNIGHT, knightSource, collect)(Knight) +export default DragSource( + ItemTypes.KNIGHT, + { + beginDrag: () => ({}), + }, + (connect: DragSourceConnector, monitor: DragSourceMonitor) => ({ + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + isDragging: monitor.isDragging(), + }), +)(Knight) diff --git a/packages/examples/src/06 Other/Drag Source Rerender/Example.tsx b/packages/examples/src/06 Other/Drag Source Rerender/Example.tsx new file mode 100644 index 0000000000..fdcf5ebb20 --- /dev/null +++ b/packages/examples/src/06 Other/Drag Source Rerender/Example.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { DragSource, ConnectDragSource } from 'react-dnd' + +interface ParentProps { + isDragging: boolean + connectDragSource: ConnectDragSource +} +const Parent: React.FC = ({ isDragging, connectDragSource }) => { + return ( + + {isDragging ? 'Dragging' : 'Drag me'} + + ) +} + +export default DragSource( + 'KNIGHT', + { + beginDrag: () => ({}), + }, + (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }), +)(Parent) + +interface ChildProps { + connect: ConnectDragSource +} + +const Child: React.FC = ({ connect, children }) => { + const [open, setOpen] = React.useState(true) + const toggle = React.useCallback(() => setOpen(!open), [open]) + + return ( +
+ + {open ? ( +
connect(node)} + style={{ + padding: 32, + marginTop: 16, + background: '#eee', + }} + > + {children} +
+ ) : null} +
+ ) +} diff --git a/packages/examples/src/06 Other/Drag Source Rerender/index.tsx b/packages/examples/src/06 Other/Drag Source Rerender/index.tsx new file mode 100644 index 0000000000..49e14d4e60 --- /dev/null +++ b/packages/examples/src/06 Other/Drag Source Rerender/index.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import Example from './Example' + +const Container: React.FC = () => { + return ( + <> +

+ Drag the box before hiding then hide it and show it again and try again. +

+
+ Note: this is more of a test-case then a usage demo. It will go away in + the future. +
+ + + ) +} +export default Container diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 76e813be7a..a30583686e 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -15,6 +15,7 @@ import sortableSimple from './04 Sortable/Simple' import sortableStressTest from './04 Sortable/Stress Test' import customizeDropEffects from './05 Customize/Drop Effects' import customizeHandlesAndPreviews from './05 Customize/Handles and Previews' +import dragSourceRerender from './06 Other/Drag Source Rerender' import otherNativeFiles from './06 Other/Native Files' export * from './isDebugMode' @@ -37,5 +38,6 @@ export const componentIndex: { 'sortable-stress-test': sortableStressTest, 'customize-drop-effects': customizeDropEffects, 'customize-handles-and-previews': customizeHandlesAndPreviews, + 'other-drag-source-rerender': dragSourceRerender, 'other-native-files': otherNativeFiles, } diff --git a/packages/react-dnd/package.json b/packages/react-dnd/package.json index 7312395abd..676854b7c8 100644 --- a/packages/react-dnd/package.json +++ b/packages/react-dnd/package.json @@ -25,7 +25,6 @@ "hoist-non-react-statics": "^3.3.0", "invariant": "^2.1.0", "lodash": "^4.17.11", - "recompose": "^0.30.0", "shallowequal": "^1.1.0" }, "devDependencies": { diff --git a/packages/react-dnd/src/DragDropContext.tsx b/packages/react-dnd/src/DragDropContext.tsx index 29ec92faba..3c4031e18a 100644 --- a/packages/react-dnd/src/DragDropContext.tsx +++ b/packages/react-dnd/src/DragDropContext.tsx @@ -10,7 +10,6 @@ import checkDecoratorArguments from './utils/checkDecoratorArguments' import { ContextComponent } from './interfaces' const invariant = require('invariant') const hoistStatics = require('hoist-non-react-statics') -const isClassComponent = require('recompose/isClassComponent').default /** * The React context type */ @@ -103,10 +102,8 @@ export function DragDropContext( public render() { return ( - + {/* If decorated is an FC, then the reff will be blank */} + ) } diff --git a/packages/react-dnd/src/DragLayer.tsx b/packages/react-dnd/src/DragLayer.tsx index 785fce834f..1054e01e9f 100644 --- a/packages/react-dnd/src/DragLayer.tsx +++ b/packages/react-dnd/src/DragLayer.tsx @@ -8,7 +8,6 @@ const hoistStatics = require('hoist-non-react-statics') const isPlainObject = require('lodash/isPlainObject') const invariant = require('invariant') const shallowEqual = require('shallowequal') -const isClassComponent = require('recompose/isClassComponent').default export default function DragLayer( collect: DragLayerCollector, @@ -91,11 +90,7 @@ export default function DragLayer( } return ( - + ) }} diff --git a/packages/react-dnd/src/DragPreviewImage.tsx b/packages/react-dnd/src/DragPreviewImage.tsx new file mode 100644 index 0000000000..ba4a0eeb84 --- /dev/null +++ b/packages/react-dnd/src/DragPreviewImage.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' +import { ConnectDragPreview } from './interfaces' + +interface DragPreviewImageProps { + connect: ConnectDragPreview + src: string +} +/* + * A utility for rendering a drag preview image + */ +const DragPreviewImage: React.FC = React.memo( + ({ connect, src }) => { + const img = new Image() + img.src = src + img.onload = () => connect(img) + return null + }, +) + +export default DragPreviewImage diff --git a/packages/react-dnd/src/DragSource.ts b/packages/react-dnd/src/DragSource.ts index fa1ccf8066..1877f4ca76 100644 --- a/packages/react-dnd/src/DragSource.ts +++ b/packages/react-dnd/src/DragSource.ts @@ -12,7 +12,7 @@ import decorateHandler from './decorateHandler' import registerSource from './registerSource' import createSourceFactory from './createSourceFactory' import DragSourceMonitorImpl from './DragSourceMonitorImpl' -import createSourceConnector from './createSourceConnector' +import SourceConnector from './SourceConnector' import isValidType from './utils/isValidType' const invariant = require('invariant') const isPlainObject = require('lodash/isPlainObject') @@ -84,9 +84,9 @@ export default function DragSource( containerDisplayName: 'DragSource', createHandler: createSource as any, registerHandler: registerSource, + createConnector: (backend: any) => new SourceConnector(backend), createMonitor: (manager: DragDropManager) => new DragSourceMonitorImpl(manager), - createConnector: createSourceConnector, DecoratedComponent, getType, collect, diff --git a/packages/react-dnd/src/DropTarget.ts b/packages/react-dnd/src/DropTarget.ts index c99e8756c3..c583152bf1 100644 --- a/packages/react-dnd/src/DropTarget.ts +++ b/packages/react-dnd/src/DropTarget.ts @@ -11,9 +11,9 @@ import checkDecoratorArguments from './utils/checkDecoratorArguments' import decorateHandler from './decorateHandler' import registerTarget from './registerTarget' import createTargetFactory from './createTargetFactory' -import createTargetConnector from './createTargetConnector' import isValidType from './utils/isValidType' import DropTargetMonitorImpl from './DropTargetMonitorImpl' +import TargetConnector from './TargetConnector' const invariant = require('invariant') const isPlainObject = require('lodash/isPlainObject') @@ -79,7 +79,7 @@ export default function DropTarget( registerHandler: registerTarget, createMonitor: (manager: DragDropManager) => new DropTargetMonitorImpl(manager), - createConnector: createTargetConnector, + createConnector: (backend: any) => new TargetConnector(backend), DecoratedComponent, getType, collect, diff --git a/packages/react-dnd/src/SourceConnector.ts b/packages/react-dnd/src/SourceConnector.ts new file mode 100644 index 0000000000..b007b4cdf6 --- /dev/null +++ b/packages/react-dnd/src/SourceConnector.ts @@ -0,0 +1,199 @@ +declare var require: any +import * as React from 'react' +import wrapConnectorHooks from './wrapConnectorHooks' +import { Backend, Unsubscribe, Identifier } from 'dnd-core' +import { isRef } from './utils/isRef' +import { DragSourceOptions, DragPreviewOptions } from './interfaces' +const shallowEqual = require('shallowequal') + +export interface Connector { + hooks: any + connectTarget: any + receiveHandlerId(handlerId: Identifier | null): void + reconnect(): void +} + +export default class SourceConnector implements Connector { + public hooks = wrapConnectorHooks({ + dragSource: ( + node: Element | React.ReactElement | React.Ref, + options?: DragSourceOptions, + ) => { + this.dragSourceOptions = options || null + if (isRef(node)) { + this.dragSourceRef = node as React.RefObject + } else { + this.dragSourceNode = node + } + this.reconnectDragSource() + }, + dragPreview: (node: any, options?: DragPreviewOptions) => { + this.dragPreviewOptions = options || null + if (isRef(node)) { + this.dragPreviewRef = node + } else { + this.dragPreviewNode = node + } + this.reconnectDragPreview() + }, + }) + private handlerId: Identifier | null = null + + // The drop target may either be attached via ref or connect function + private dragSourceRef: React.RefObject | null = null + private dragSourceNode: any + private dragSourceOptionsInternal: DragSourceOptions | null = null + private dragSourceUnsubscribe: Unsubscribe | undefined + + // The drag preview may either be attached via ref or connect function + private dragPreviewRef: React.RefObject | null = null + private dragPreviewNode: any + private dragPreviewOptionsInternal: DragPreviewOptions | null = null + private dragPreviewUnsubscribe: Unsubscribe | undefined + + private lastConnectedHandlerId: Identifier | null = null + private lastConnectedDragSource: any = null + private lastConnectedDragSourceOptions: any = null + private lastConnectedDragPreview: any = null + private lastConnectedDragPreviewOptions: any = null + + constructor(private backend: Backend) {} + + public receiveHandlerId(newHandlerId: Identifier | null) { + if (this.handlerId === newHandlerId) { + return + } + + this.handlerId = newHandlerId + this.reconnect() + } + + get connectTarget() { + return this.dragSource + } + + public get dragSourceOptions() { + return this.dragSourceOptionsInternal + } + public set dragSourceOptions(options: DragSourceOptions | null) { + this.dragSourceOptionsInternal = options + } + + public get dragPreviewOptions() { + return this.dragPreviewOptionsInternal + } + + public set dragPreviewOptions(options: DragPreviewOptions | null) { + this.dragPreviewOptionsInternal = options + } + + public reconnect() { + this.reconnectDragSource() + this.reconnectDragPreview() + } + + private reconnectDragSource() { + const dragSource = this.dragSource + if (!this.handlerId || !dragSource) { + return + } + + // if nothing has changed then don't resubscribe + if ( + this.didHandlerIdChange() || + this.didConnectedDragSourceChange() || + this.didDragSourceOptionsChange() + ) { + this.disconnectDragSource() + this.lastConnectedHandlerId = this.handlerId + this.lastConnectedDragSource = dragSource + this.lastConnectedDragSourceOptions = this.dragSourceOptions + this.dragSourceUnsubscribe = this.backend.connectDragSource( + this.handlerId, + dragSource, + this.dragSourceOptions, + ) + } + } + + private reconnectDragPreview() { + const dragPreview = this.dragPreview + if (!this.handlerId || !dragPreview) { + return + } + + // if nothing has changed then don't resubscribe + if ( + this.didHandlerIdChange() || + this.didConnectedDragPreviewChange() || + this.didDragPreviewOptionsChange() + ) { + this.disconnectDragPreview() + this.lastConnectedHandlerId = this.handlerId + this.lastConnectedDragPreview = dragPreview + this.lastConnectedDragPreviewOptions = this.dragPreviewOptions + this.dragPreviewUnsubscribe = this.backend.connectDragPreview( + this.handlerId, + dragPreview, + this.dragPreviewOptions, + ) + } + } + + private didHandlerIdChange(): boolean { + return this.lastConnectedHandlerId !== this.handlerId + } + + private didConnectedDragSourceChange(): boolean { + return this.lastConnectedDragSource !== this.dragSource + } + + private didConnectedDragPreviewChange(): boolean { + return this.lastConnectedDragPreview !== this.dragPreview + } + + private didDragSourceOptionsChange(): boolean { + return !shallowEqual( + this.lastConnectedDragSourceOptions, + this.dragSourceOptions, + ) + } + + private didDragPreviewOptionsChange(): boolean { + return !shallowEqual( + this.lastConnectedDragPreviewOptions, + this.dragPreviewOptions, + ) + } + + private disconnectDragSource() { + if (this.dragSourceUnsubscribe) { + this.dragSourceUnsubscribe() + this.dragSourceUnsubscribe = undefined + this.dragPreviewNode = null + this.dragPreviewRef = null + } + } + + private disconnectDragPreview() { + if (this.dragPreviewUnsubscribe) { + this.dragPreviewUnsubscribe() + this.dragPreviewUnsubscribe = undefined + this.dragPreviewNode = null + this.dragPreviewRef = null + } + } + + private get dragSource() { + return ( + this.dragSourceNode || (this.dragSourceRef && this.dragSourceRef.current) + ) + } + + private get dragPreview() { + return ( + this.dragPreviewNode || + (this.dragPreviewRef && this.dragPreviewRef.current) + ) + } +} diff --git a/packages/react-dnd/src/TargetConnector.ts b/packages/react-dnd/src/TargetConnector.ts new file mode 100644 index 0000000000..ea8094c766 --- /dev/null +++ b/packages/react-dnd/src/TargetConnector.ts @@ -0,0 +1,107 @@ +declare var require: any +import * as React from 'react' +import wrapConnectorHooks from './wrapConnectorHooks' +import { Backend, Unsubscribe, Identifier } from 'dnd-core' +import { isRef } from './utils/isRef' +const shallowEqual = require('shallowequal') + +import { Connector } from './SourceConnector' + +export default class TargetConnector implements Connector { + public hooks = wrapConnectorHooks({ + dropTarget: (node: any, options: any) => { + this.dropTargetOptions = options + if (isRef(node)) { + this.dropTargetRef = node + } else { + this.dropTargetNode = node + } + this.reconnect() + }, + }) + + private handlerId: Identifier | null = null + // The drop target may either be attached via ref or connect function + private dropTargetRef: React.RefObject | null = null + private dropTargetNode: any + private dropTargetOptionsInternal: any = null + private unsubscribeDropTarget: Unsubscribe | undefined + + private lastConnectedHandlerId: Identifier | null = null + private lastConnectedDropTarget: any = null + private lastConnectedDropTargetOptions: any = null + + constructor(private backend: Backend) {} + + public get connectTarget() { + return this.dropTarget + } + + public reconnect() { + const dropTarget = this.dropTarget + if (!this.handlerId || !dropTarget) { + return + } + // if nothing has changed then don't resubscribe + if ( + this.didHandlerIdChange() || + this.didDropTargetChange() || + this.didOptionsChange() + ) { + this.disconnectDropTarget() + this.lastConnectedHandlerId = this.handlerId + this.lastConnectedDropTarget = dropTarget + this.lastConnectedDropTargetOptions = this.dropTargetOptions + + this.unsubscribeDropTarget = this.backend.connectDropTarget( + this.handlerId, + dropTarget, + this.dropTargetOptions, + ) + } + } + + public receiveHandlerId(newHandlerId: Identifier | null) { + if (newHandlerId === this.handlerId) { + return + } + + this.handlerId = newHandlerId + this.reconnect() + } + + public get dropTargetOptions() { + return this.dropTargetOptionsInternal + } + public set dropTargetOptions(options: any) { + this.dropTargetOptionsInternal = options + } + + private didHandlerIdChange(): boolean { + return this.lastConnectedHandlerId !== this.handlerId + } + + private didDropTargetChange(): boolean { + return this.lastConnectedDropTarget !== this.dropTarget + } + + private didOptionsChange(): boolean { + return !shallowEqual( + this.lastConnectedDropTargetOptions, + this.dropTargetOptions, + ) + } + + private disconnectDropTarget() { + if (this.unsubscribeDropTarget) { + this.unsubscribeDropTarget() + this.unsubscribeDropTarget = undefined + } + } + + private get dropTarget() { + return ( + this.dropTargetNode || (this.dropTargetRef && this.dropTargetRef.current) + ) + } +} diff --git a/packages/react-dnd/src/createSourceConnector.ts b/packages/react-dnd/src/createSourceConnector.ts deleted file mode 100644 index b0af087121..0000000000 --- a/packages/react-dnd/src/createSourceConnector.ts +++ /dev/null @@ -1,125 +0,0 @@ -declare var require: any -import * as React from 'react' -import wrapConnectorHooks from './wrapConnectorHooks' -import { Backend, Unsubscribe, Identifier } from 'dnd-core' -import { isRef } from './hooks/util' -import { DragSourceOptions, DragPreviewOptions } from './interfaces' -const shallowEqual = require('shallowequal') - -export default function createSourceConnector(backend: Backend) { - let handlerId: Identifier - - // The drop target may either be attached via ref or connect function - let dragSourceRef: React.RefObject | null = null - let dragSourceNode: any - let dragSourceOptions: any - let disconnectDragSource: Unsubscribe | undefined - - // The drag preview may either be attached via ref or connect function - let dragPreviewRef: React.RefObject | null = null - let dragPreviewNode: any - let dragPreviewOptions: any - let disconnectDragPreview: Unsubscribe | undefined - - let lastConnectedHandlerId: Identifier | null = null - let lastConnectedDragSource: any = null - let lastConnectedDragSourceOptions: any = null - let lastConnectedDragPreview: any = null - let lastConnectedDragPreviewOptions: any = null - - function reconnectDragSource() { - const dragSource = - dragSourceNode || (dragSourceRef && dragSourceRef.current) - if (!handlerId || !dragSource) { - return - } - - // if nothing has changed then don't resubscribe - if ( - lastConnectedHandlerId !== handlerId || - lastConnectedDragSource !== dragSource || - !shallowEqual(lastConnectedDragSourceOptions, dragSourceOptions) - ) { - if (disconnectDragSource) { - disconnectDragSource() - disconnectDragSource = undefined - } - - lastConnectedHandlerId = handlerId - lastConnectedDragSource = dragSource - lastConnectedDragSourceOptions = dragSourceOptions - disconnectDragSource = backend.connectDragSource( - handlerId, - dragSource, - dragSourceOptions, - ) - } - } - - function reconnectDragPreview() { - const dragPreview = - dragPreviewNode || (dragPreviewRef && dragPreviewRef.current) - if (!handlerId || !dragPreview) { - return - } - - // if nothing has changed then don't resubscribe - if ( - lastConnectedHandlerId !== handlerId || - lastConnectedDragPreview !== dragPreview || - !shallowEqual(lastConnectedDragPreviewOptions, dragPreviewOptions) - ) { - if (disconnectDragPreview) { - disconnectDragPreview() - disconnectDragPreview = undefined - } - lastConnectedHandlerId = handlerId - lastConnectedDragPreview = dragPreview - lastConnectedDragPreviewOptions = dragPreviewOptions - disconnectDragPreview = backend.connectDragPreview( - handlerId, - dragPreview, - dragPreviewOptions, - ) - } - } - - function receiveHandlerId(newHandlerId: Identifier) { - if (handlerId === newHandlerId) { - return - } - - handlerId = newHandlerId - reconnectDragSource() - reconnectDragPreview() - } - - return { - receiveHandlerId, - hooks: wrapConnectorHooks({ - dragSource: ( - node: Element | React.ReactElement | React.Ref, - options?: DragSourceOptions, - ) => { - dragSourceOptions = options - if (isRef(node)) { - dragSourceRef = node as React.RefObject - } else { - dragSourceNode = node - } - }, - dragPreview: (node: any, options?: DragPreviewOptions) => { - dragPreviewOptions = options - if (isRef(node)) { - dragPreviewRef = node - } else { - dragPreviewNode = node - } - }, - }), - reconnect: () => { - reconnectDragSource() - reconnectDragPreview() - }, - } -} diff --git a/packages/react-dnd/src/createSourceFactory.ts b/packages/react-dnd/src/createSourceFactory.ts index 7138532096..bb38761937 100644 --- a/packages/react-dnd/src/createSourceFactory.ts +++ b/packages/react-dnd/src/createSourceFactory.ts @@ -1,6 +1,5 @@ declare var require: any declare var process: any - import * as React from 'react' import { DragSource, DragDropMonitor } from 'dnd-core' import { DragSourceSpec, DragSourceMonitor } from './interfaces' @@ -24,7 +23,7 @@ class SourceImpl implements Source { private ref: React.RefObject, ) {} - public receiveProps(props: any) { + public receiveProps(props: Props) { this.props = props } diff --git a/packages/react-dnd/src/createTargetConnector.ts b/packages/react-dnd/src/createTargetConnector.ts deleted file mode 100644 index 99d58971c5..0000000000 --- a/packages/react-dnd/src/createTargetConnector.ts +++ /dev/null @@ -1,71 +0,0 @@ -declare var require: any -import * as React from 'react' -import wrapConnectorHooks from './wrapConnectorHooks' -import { Backend, Unsubscribe, Identifier } from 'dnd-core' -import { isRef } from './hooks/util' -const shallowEqual = require('shallowequal') - -export default function createTargetConnector(backend: Backend) { - let handlerId: Identifier - // The drop target may either be attached via ref or connect function - let dropTargetRef: React.RefObject | null = null - let dropTargetNode: any - let dropTargetOptions: any - let disconnectDropTarget: Unsubscribe | undefined - - let lastConnectedHandlerId: Identifier | null = null - let lastConnectedDropTarget: any = null - let lastConnectedDropTargetOptions: any = null - - function reconnectDropTarget() { - const dropTarget = - dropTargetNode || (dropTargetRef && dropTargetRef.current) - if (!handlerId || !dropTarget) { - return - } - // if nothing has changed then don't resubscribe - if ( - lastConnectedHandlerId !== handlerId || - lastConnectedDropTarget !== dropTarget || - !shallowEqual(lastConnectedDropTargetOptions, dropTargetOptions) - ) { - if (disconnectDropTarget) { - disconnectDropTarget() - disconnectDropTarget = undefined - } - lastConnectedHandlerId = handlerId - lastConnectedDropTarget = dropTarget - lastConnectedDropTargetOptions = dropTargetOptions - - disconnectDropTarget = backend.connectDropTarget( - handlerId, - dropTarget, - dropTargetOptions, - ) - } - } - - function receiveHandlerId(newHandlerId: Identifier) { - if (newHandlerId === handlerId) { - return - } - - handlerId = newHandlerId - reconnectDropTarget() - } - - return { - receiveHandlerId, - hooks: wrapConnectorHooks({ - dropTarget: (node: any, options: any) => { - dropTargetOptions = options - if (isRef(node)) { - dropTargetRef = node - } else { - dropTargetNode = node - } - }, - }), - reconnect: reconnectDropTarget, - } -} diff --git a/packages/react-dnd/src/createTargetFactory.ts b/packages/react-dnd/src/createTargetFactory.ts index a7c722d4a1..e8e4e8a176 100644 --- a/packages/react-dnd/src/createTargetFactory.ts +++ b/packages/react-dnd/src/createTargetFactory.ts @@ -23,11 +23,11 @@ class TargetImpl implements Target { private ref: React.RefObject, ) {} - public receiveProps(props: any) { + public receiveProps(props: Props) { this.props = props } - public receiveMonitor(monitor: any) { + public receiveMonitor(monitor: DropTargetMonitor) { this.monitor = monitor } diff --git a/packages/react-dnd/src/decorateHandler.tsx b/packages/react-dnd/src/decorateHandler.tsx index bc34f102cd..21f341ca1c 100644 --- a/packages/react-dnd/src/decorateHandler.tsx +++ b/packages/react-dnd/src/decorateHandler.tsx @@ -10,7 +10,7 @@ import { CompositeDisposable, SerialDisposable, } from './utils/disposables' -const isClassComponent = require('recompose/isClassComponent').default +import { Connector } from './SourceConnector' const isPlainObject = require('lodash/isPlainObject') const invariant = require('invariant') const hoistStatics = require('hoist-non-react-statics') @@ -39,12 +39,6 @@ interface Handler { receiveProps(props: Props): void } -interface HandlerConnector extends HandlerReceiver { - hooks: any[] - receiveHandlerId: (handleId: any) => void - reconnect: () => void -} - export default function decorateHandler({ DecoratedComponent, createHandler, @@ -67,10 +61,11 @@ export default function decorateHandler({ public static DecoratedComponent = DecoratedComponent public static displayName = `${containerDisplayName}(${displayName})` + private decoratedRef = React.createRef() private handlerId: string | undefined private manager: DragDropManager | undefined private handlerMonitor: HandlerReceiver | undefined - private handlerConnector: HandlerConnector | undefined + private handlerConnector: Connector | undefined private handler: Handler | undefined private disposable: any private currentType: any @@ -87,10 +82,11 @@ export default function decorateHandler({ } public getDecoratedComponentInstance() { - if (!this.handler) { - return null - } - return this.handler.ref.current as any + invariant( + this.decoratedRef.current, + 'In order to access an instance of the decorated component it can not be a stateless component.', + ) + return this.decoratedRef.current as any } public shouldComponentUpdate(nextProps: any, nextState: any) { @@ -137,7 +133,7 @@ export default function decorateHandler({ this.currentType = type - const { handlerId, unregister } = registerHandler( + const [handlerId, unregister] = registerHandler( type, this.handler, this.manager, @@ -208,16 +204,12 @@ export default function decorateHandler({ } this.receiveDragDropManager(dragDropManager) requestAnimationFrame(() => this.handlerConnector!.reconnect()) - return ( ) }} @@ -238,10 +230,9 @@ export default function decorateHandler({ displayName, displayName, ) - const itemRef = React.createRef() this.handlerMonitor = createMonitor(dragDropManager) this.handlerConnector = createConnector(dragDropManager.getBackend()) - this.handler = createHandler(this.handlerMonitor, itemRef) + this.handler = createHandler(this.handlerMonitor, this.decoratedRef) } } diff --git a/packages/react-dnd/src/hooks/index.ts b/packages/react-dnd/src/hooks/index.ts index fb5d78554c..7fcec0f941 100644 --- a/packages/react-dnd/src/hooks/index.ts +++ b/packages/react-dnd/src/hooks/index.ts @@ -1,4 +1,3 @@ export * from './useDrag' export * from './useDrop' export * from './useDragLayer' -export * from './useDragPreview' diff --git a/packages/react-dnd/src/hooks/internal/drag.ts b/packages/react-dnd/src/hooks/internal/drag.ts new file mode 100644 index 0000000000..010a34eb0f --- /dev/null +++ b/packages/react-dnd/src/hooks/internal/drag.ts @@ -0,0 +1,83 @@ +declare var require: any +import { useEffect, useMemo, MutableRefObject } from 'react' +import { + DragSourceHookSpec, + DragObjectWithType, + DragSourceMonitor, +} from '../../interfaces' +import { DragDropMonitor, DragSource } from 'dnd-core' +import registerSource from '../../registerSource' +import { useDragDropManager } from './useDragDropManager' +import DragSourceMonitorImpl from '../../DragSourceMonitorImpl' +import SourceConnector from '../../SourceConnector' +const invariant = require('invariant') + +export function useDragSourceMonitor(): [DragSourceMonitor, SourceConnector] { + const manager = useDragDropManager() + const monitor = useMemo(() => new DragSourceMonitorImpl(manager), [manager]) + const connector = useMemo(() => new SourceConnector(manager.getBackend()), [ + manager, + ]) + return [monitor, connector] +} + +export function useDragHandler< + DragObject extends DragObjectWithType, + DropResult, + CustomProps +>( + spec: MutableRefObject< + DragSourceHookSpec + >, + monitor: DragSourceMonitor, + connector: any, +) { + const manager = useDragDropManager() + + // Can't use createSourceFactory, as semantics are different + const handler = useMemo(() => { + return { + beginDrag() { + const { begin, item } = spec.current + if (begin) { + const beginResult = begin(monitor) + invariant( + beginResult == null || typeof beginResult === 'object', + 'dragSpec.begin() must either return an object, undefined, or null', + ) + return beginResult || item || {} + } + return item || {} + }, + canDrag() { + const { canDrag } = spec.current + return canDrag ? canDrag(monitor) : true + }, + isDragging(globalMonitor: DragDropMonitor, target) { + const { isDragging } = spec.current + return isDragging + ? isDragging(monitor) + : target === globalMonitor.getSourceId() + }, + endDrag() { + const { end } = spec.current + if (end) { + end(monitor.getItem(), monitor) + } + connector.reconnect() + }, + } as DragSource + }, []) + + useEffect(function registerHandler() { + // console.log('Register Handler') + const [handlerId, unregister] = registerSource( + spec.current.item.type, + handler, + manager, + ) + monitor.receiveHandlerId(handlerId) + connector.receiveHandlerId(handlerId) + return unregister + }, []) +} diff --git a/packages/react-dnd/src/hooks/internal/drop.ts b/packages/react-dnd/src/hooks/internal/drop.ts new file mode 100644 index 0000000000..fc71b87ea7 --- /dev/null +++ b/packages/react-dnd/src/hooks/internal/drop.ts @@ -0,0 +1,72 @@ +import { + DragObjectWithType, + DropTargetMonitor, + DropTargetHookSpec, +} from '../../interfaces' +import { useEffect, useMemo, MutableRefObject } from 'react' +import { DropTarget } from 'dnd-core' +import registerTarget from '../../registerTarget' +import { useDragDropManager } from './useDragDropManager' +import TargetConnector from '../../TargetConnector' +import DropTargetMonitorImpl from '../../DropTargetMonitorImpl' + +export function useDropTargetMonitor(): [DropTargetMonitor, TargetConnector] { + const manager = useDragDropManager() + const monitor = useMemo(() => new DropTargetMonitorImpl(manager), [manager]) + const connector = useMemo(() => new TargetConnector(manager.getBackend()), [ + manager, + ]) + return [monitor, connector] +} + +export function useDropHandler< + DragObject extends DragObjectWithType, + DropResult, + CustomProps +>( + spec: MutableRefObject< + DropTargetHookSpec + >, + monitor: DropTargetMonitor, + connector: any, +) { + const manager = useDragDropManager() + + // Can't use createSourceFactory, as semantics are different + const handler = useMemo(() => { + // console.log('create drop target handler') + return { + canDrop() { + const { canDrop } = spec.current + return canDrop ? canDrop(monitor.getItem(), monitor) : true + }, + hover() { + const { hover } = spec.current + if (hover) { + hover(monitor.getItem(), monitor) + } + }, + drop() { + const { drop } = spec.current + if (drop) { + return drop(monitor.getItem(), monitor) + } + }, + } as DropTarget + }, [monitor]) + + useEffect( + function registerHandler() { + // console.log('register droptarget handler') + const [handlerId, unregister] = registerTarget( + spec.current.accept, + handler, + manager, + ) + monitor.receiveHandlerId(handlerId) + connector.receiveHandlerId(handlerId) + return unregister + }, + [monitor, connector], + ) +} diff --git a/packages/react-dnd/src/hooks/internal/useCollector.ts b/packages/react-dnd/src/hooks/internal/useCollector.ts index 23b456ed62..b3cf6b4f28 100644 --- a/packages/react-dnd/src/hooks/internal/useCollector.ts +++ b/packages/react-dnd/src/hooks/internal/useCollector.ts @@ -1,18 +1,32 @@ -declare var require: any +// declare var require: any +// const shallowEqual = require('shallowequal') import { useState } from 'react' -const shallowEqual = require('shallowequal') +/** + * + * @param monitor The monitor to colelct state from + * @param collect The collecting function + * @param onUpdate A method to invoke when updates occur + */ export function useCollector( monitor: T, collect: (monitor: T) => S, + onUpdate?: () => void, ): [S, () => void] { const [collected, setCollected] = useState(() => collect(monitor)) const updateCollected = () => { const nextValue = collect(monitor) - if (!shallowEqual(collected, nextValue)) { - setCollected(nextValue) + // TODO: we need this shallowequal check to work + // so that we can operate performantly, but the examples + // are broken with it in currently + + // if (!shallowEqual(collected, nextValue)) { + setCollected(nextValue) + if (onUpdate) { + onUpdate() } + // } } return [collected, updateCollected] diff --git a/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts b/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts deleted file mode 100644 index 1a3ba85874..0000000000 --- a/packages/react-dnd/src/hooks/internal/useDragSourceMonitor.ts +++ /dev/null @@ -1,78 +0,0 @@ -declare var require: any -import { useMemo, useEffect, useRef } from 'react' -import { DragSource, DragDropManager } from 'dnd-core' -import { - DragSourceHookSpec, - DragSourceMonitor, - DragObjectWithType, -} from '../../interfaces' -import DragSourceMonitorImpl from '../../DragSourceMonitorImpl' -import registerSource from '../../registerSource' -const invariant = require('invariant') - -export function useDragSourceMonitor< - DragObject extends DragObjectWithType, - DropResult, - CustomProps ->( - manager: DragDropManager, - sourceSpec: DragSourceHookSpec, -): DragSourceMonitor { - const sourceSpecRef = useRef(sourceSpec) - - useEffect(() => { - sourceSpecRef.current = sourceSpec - }) - - const monitor = useMemo(() => new DragSourceMonitorImpl(manager), [manager]) - useEffect( - function registerSourceWithMonitor() { - const { handlerId, unregister } = registerSource( - sourceSpec.item.type, - handler, - manager, - ) - monitor.receiveHandlerId(handlerId) - return unregister - }, - [monitor], - ) - - // Can't use createSourceFactory, as semantics are different - const handler = useMemo( - () => - ({ - beginDrag() { - const { begin, item } = sourceSpecRef.current - if (begin) { - const beginResult = begin(monitor) - invariant( - beginResult == null || typeof beginResult === 'object', - 'dragSpec.begin() must either return an object, undefined, or null', - ) - return beginResult || item || {} - } - return item || {} - }, - canDrag() { - const { canDrag } = sourceSpecRef.current - return canDrag ? canDrag(monitor) : true - }, - isDragging(globalMonitor, target) { - const { isDragging } = sourceSpecRef.current - return isDragging - ? isDragging(monitor) - : target === globalMonitor.getSourceId() - }, - endDrag() { - const { end } = sourceSpecRef.current - if (end) { - end(monitor.getItem(), monitor) - } - }, - } as DragSource), - [], - ) - - return monitor -} diff --git a/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts b/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts deleted file mode 100644 index b5d390a137..0000000000 --- a/packages/react-dnd/src/hooks/internal/useDropTargetMonitor.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' -import { DropTargetHookSpec, DropTargetMonitor } from '../../interfaces' -import { DropTarget, DragDropManager } from 'dnd-core' -import DropTargetMonitorImpl from '../../DropTargetMonitorImpl' -import registerTarget from '../../registerTarget' - -export function useDropTargetMonitor( - manager: DragDropManager, - targetSpec: DropTargetHookSpec, -): DropTargetMonitor { - const targetSpecRef = React.useRef(targetSpec) - - React.useEffect(function updateDropTargetSpec() { - targetSpecRef.current = targetSpec - }) - - const monitor = React.useMemo(() => new DropTargetMonitorImpl(manager), [ - manager, - ]) - React.useEffect( - function registerTargetWithMonitor() { - const { handlerId, unregister } = registerTarget( - targetSpec.accept, - handler, - manager, - ) - monitor.receiveHandlerId(handlerId) - return unregister - }, - [monitor], - ) - - // Can't use createSourceFactory, as semantics are different - const handler = React.useMemo( - () => - ({ - canDrop() { - const { canDrop } = targetSpecRef.current - return canDrop ? canDrop(monitor.getItem(), monitor) : true - }, - hover() { - const { hover } = targetSpecRef.current - if (hover) { - hover(monitor.getItem(), monitor) - } - }, - drop() { - const { drop } = targetSpecRef.current - if (drop) { - return drop(monitor.getItem(), monitor) - } - }, - } as DropTarget), - [], - ) - - return monitor -} diff --git a/packages/react-dnd/src/hooks/internal/useMonitorOutput.ts b/packages/react-dnd/src/hooks/internal/useMonitorOutput.ts index 09c4968aeb..427c9ab7cc 100644 --- a/packages/react-dnd/src/hooks/internal/useMonitorOutput.ts +++ b/packages/react-dnd/src/hooks/internal/useMonitorOutput.ts @@ -1,25 +1,27 @@ import { useEffect } from 'react' import { useCollector } from './useCollector' -import { HandlerManager } from '../../interfaces' -import { DragDropMonitor } from 'dnd-core' +import { HandlerManager, MonitorEventEmitter } from '../../interfaces' -export function useMonitorOutput< - Monitor extends DragDropMonitor & HandlerManager, - Collected ->(monitor: Monitor, collect: (monitor: Monitor) => Collected): Collected { - const [collected, updateCollected] = useCollector(monitor, collect) +export function useMonitorOutput( + monitor: Monitor & MonitorEventEmitter, + collect: (monitor: Monitor) => Collected, + onCollect?: () => void, +): Collected { + const [collected, updateCollected] = useCollector(monitor, collect, onCollect) - // This runs on every render. There will be ways to optimise this, but for - // now, this is the most correct thing to do. - useEffect(function subscribeToMonitorStateChange() { - const handlerId = monitor.getHandlerId() - if (handlerId == null) { - return undefined - } - return monitor.subscribeToStateChange(updateCollected, { - handlerIds: [handlerId], - }) - }) + useEffect( + function subscribeToMonitorStateChange() { + const handlerId = monitor.getHandlerId() + if (handlerId == null) { + return undefined + } + const unsubscribe = monitor.subscribeToStateChange(updateCollected, { + handlerIds: [handlerId], + }) + return unsubscribe + }, + [monitor], + ) return collected } diff --git a/packages/react-dnd/src/hooks/internal/useRefObject.ts b/packages/react-dnd/src/hooks/internal/useRefObject.ts new file mode 100644 index 0000000000..7682f0212b --- /dev/null +++ b/packages/react-dnd/src/hooks/internal/useRefObject.ts @@ -0,0 +1,9 @@ +import { RefObject, useRef, useEffect } from 'react' + +export function useRefObject(input: T): RefObject { + const ref = useRef(input) + useEffect(() => { + ref.current = input + }, [input]) + return ref +} diff --git a/packages/react-dnd/src/hooks/useDrag.ts b/packages/react-dnd/src/hooks/useDrag.ts index d41d948c0f..0f7561911b 100644 --- a/packages/react-dnd/src/hooks/useDrag.ts +++ b/packages/react-dnd/src/hooks/useDrag.ts @@ -1,10 +1,13 @@ declare var require: any -import { useEffect, useRef } from 'react' -import { DragSourceHookSpec, DragObjectWithType } from '../interfaces' -import { useDragSourceMonitor } from './internal/useDragSourceMonitor' -import { useDragDropManager } from './internal/useDragDropManager' -import { Ref, isRef } from './util' +import { + DragSourceHookSpec, + DragObjectWithType, + ConnectDragSource, + ConnectDragPreview, +} from '../interfaces' import { useMonitorOutput } from './internal/useMonitorOutput' +import { useDragSourceMonitor, useDragHandler } from './internal/drag' +import { useEffect, useRef, useMemo } from 'react' const invariant = require('invariant') /** @@ -17,50 +20,35 @@ export function useDrag< CollectedProps >( spec: DragSourceHookSpec, -): [CollectedProps, React.RefObject] { - const { item, options, preview, previewOptions, collect } = spec - let { ref } = spec - invariant(item != null, 'item must be defined') - invariant(item.type != null, 'item type must be defined') - const manager = useDragDropManager() - const backend = manager.getBackend() - const monitor = useDragSourceMonitor( - manager, - spec, - ) - if (!ref) { - ref = useRef(null) - } +): [CollectedProps, ConnectDragSource, ConnectDragPreview] { + const specRef = useRef(spec) + + // TODO: wire options into createSourceConnector + invariant(spec.item != null, 'item must be defined') + invariant(spec.item.type != null, 'item type must be defined') - /* - * Connect the Drag Source Element to the Backend - */ - useEffect(function connectDragSource() { - const node = ref!.current - return backend.connectDragSource(monitor.getHandlerId(), node, options) - }, []) + const [monitor, connector] = useDragSourceMonitor() + useDragHandler(specRef, monitor, connector) - /* - * Connect the Drag Preview Element to the Backend - */ - useEffect( - function connectDragPreview() { - if (preview) { - const previewNode = isRef(preview) - ? (preview as Ref).current - : preview - return backend.connectDragPreview( - monitor.getHandlerId(), - previewNode, - previewOptions, - ) - } - }, - [preview && (preview as Ref).current], + const result: CollectedProps = useMonitorOutput( + monitor, + specRef.current.collect || (() => ({} as CollectedProps)), + () => connector.reconnect(), ) - const result: CollectedProps & { ref: React.RefObject } = collect - ? (useMonitorOutput(monitor as any, collect as any) as any) - : (({} as CollectedProps) as any) - return [result, ref] + const connectDragSource = useMemo(() => connector.hooks.dragSource(), [ + connector, + ]) + const connectDragPreview = useMemo(() => connector.hooks.dragPreview(), [ + connector, + ]) + useEffect(() => { + connector.dragSourceOptions = specRef.current.options || null + connector.reconnect() + }, [connector]) + useEffect(() => { + connector.dragPreviewOptions = specRef.current.previewOptions || null + connector.reconnect() + }, [connector]) + return [result, connectDragSource, connectDragPreview] } diff --git a/packages/react-dnd/src/hooks/useDragPreview.ts b/packages/react-dnd/src/hooks/useDragPreview.ts deleted file mode 100644 index 5f383fa9b5..0000000000 --- a/packages/react-dnd/src/hooks/useDragPreview.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react' -import { createPortal } from 'react-dom' - -/** - * Hook for showing a dragPreview - * @param DragPreview The drag preview component to render - */ -export function useDragPreview( - DragPreview: React.RefForwardingComponent, -): [React.FC, React.RefObject] { - // drag previews won't have layered functionality, so we can create the ref for them - // here - const ref = React.useRef(null) - - // render the dragPreview into a detached element to prevent it from appearing too early - const dragPreviewRoot = document.createElement('div') - const portaledComponent = (props: Props) => { - const sendProps = { ...props, ref } - return createPortal( - React.createElement(DragPreview, sendProps), - dragPreviewRoot, - ) - } - - return [portaledComponent, ref] -} diff --git a/packages/react-dnd/src/hooks/useDrop.ts b/packages/react-dnd/src/hooks/useDrop.ts index 01e9459e78..08b55ee327 100644 --- a/packages/react-dnd/src/hooks/useDrop.ts +++ b/packages/react-dnd/src/hooks/useDrop.ts @@ -1,43 +1,44 @@ declare var require: any -import { useEffect, useRef } from 'react' -import { DropTargetHookSpec } from '../interfaces' -import { useDragDropManager } from './internal/useDragDropManager' -import { useDropTargetMonitor } from './internal/useDropTargetMonitor' +import { + DropTargetHookSpec, + ConnectDropTarget, + DragObjectWithType, +} from '../interfaces' import { useMonitorOutput } from './internal/useMonitorOutput' +import { useDropHandler, useDropTargetMonitor } from './internal/drop' +import { useEffect, useRef, useMemo } from 'react' const invariant = require('invariant') /** * useDropTarget Hook (This API is experimental and subject to breaking changes in non-breaking versions) * @param spec The drop target specification */ -export function useDrop( +export function useDrop< + DragObject extends DragObjectWithType, + DropResult, + CollectedProps +>( spec: DropTargetHookSpec, -): [CollectedProps, React.RefObject] { - const { accept, options, collect } = spec - invariant(accept != null, 'accept must be defined') - let { ref } = spec - if (!ref) { - ref = useRef(null) - } +): [CollectedProps, ConnectDropTarget] { + const specRef = useRef(spec) + invariant(spec.accept != null, 'accept must be defined') - const manager = useDragDropManager() - const backend = manager.getBackend() - const monitor = useDropTargetMonitor(manager, spec) + const [monitor, connector] = useDropTargetMonitor() + useDropHandler(specRef, monitor, connector) - /* - * Connect the Drop Target Element to the Backend - */ - useEffect(function connectDropTarget() { - if (ref!.current) { - const node = ref!.current - if (node) { - return backend.connectDropTarget(monitor.getHandlerId(), node, options) - } - } - }) + const result: CollectedProps = useMonitorOutput( + monitor, + specRef.current.collect || (() => ({} as CollectedProps)), + () => connector.reconnect(), + ) - const result: CollectedProps & { ref: React.RefObject } = collect - ? (useMonitorOutput(monitor as any, collect as any) as any) - : (({} as CollectedProps) as any) - return [result, ref] + const connectDropTarget = useMemo(() => connector.hooks.dropTarget(), [ + connector, + ]) + + useEffect(() => { + connector.dropTargetOptions = spec.options || null + connector.reconnect() + }, [spec.options]) + return [result, connectDropTarget] } diff --git a/packages/react-dnd/src/index.ts b/packages/react-dnd/src/index.ts index 7aa8b8d27b..49329dd6df 100644 --- a/packages/react-dnd/src/index.ts +++ b/packages/react-dnd/src/index.ts @@ -7,12 +7,13 @@ export { export { default as DragLayer } from './DragLayer' export { default as DragSource } from './DragSource' export { default as DropTarget } from './DropTarget' +export { default as DragPreviewImage } from './DragPreviewImage' + export * from './interfaces' -import { useDrag, useDragLayer, useDrop, useDragPreview } from './hooks' +import { useDrag, useDragLayer, useDrop } from './hooks' export const __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ = { useDrag, useDragLayer, useDrop, - useDragPreview, } diff --git a/packages/react-dnd/src/interfaces/classApi.ts b/packages/react-dnd/src/interfaces/classApi.ts index 0e958ca55e..e830a2dd19 100644 --- a/packages/react-dnd/src/interfaces/classApi.ts +++ b/packages/react-dnd/src/interfaces/classApi.ts @@ -116,19 +116,20 @@ export interface DragSourceSpec { isDragging?: (props: Props, monitor: DragSourceMonitor) => boolean } -export type ConnectedElement = +export type ConnectableElement = | React.RefObject | React.ReactElement | Element | null -export type DragElementWrapper = ( - elementOrNode: ConnectedElement, +export type DragElementWrapper = ( + elementOrNode: ConnectableElement, options?: Options, -) => React.ReactElement +) => React.ReactElement | null export type ConnectDragSource = DragElementWrapper export type ConnectDragPreview = DragElementWrapper +export type ConnectDropTarget = DragElementWrapper /** * DragSourceConnector is an object passed to a collecting function of the DragSource. @@ -170,10 +171,6 @@ export interface DropTargetConnector { dropTarget(): ConnectDropTarget } -export type ConnectDropTarget = ( - elementOrNode: ConnectedElement, -) => React.ReactElement - export type DragSourceCollector = ( connect: DragSourceConnector, monitor: DragSourceMonitor, diff --git a/packages/react-dnd/src/interfaces/hooksApi.ts b/packages/react-dnd/src/interfaces/hooksApi.ts index a30119d6e5..11963abc07 100644 --- a/packages/react-dnd/src/interfaces/hooksApi.ts +++ b/packages/react-dnd/src/interfaces/hooksApi.ts @@ -1,6 +1,5 @@ import { TargetType, SourceType } from 'dnd-core' import { DropTargetMonitor, DragSourceMonitor } from './monitors' -import { RefObject } from 'react' import { DragSourceOptions, DragPreviewOptions } from './options' export interface DragSourceHookSpec< @@ -8,12 +7,6 @@ export interface DragSourceHookSpec< DropResult, CollectedProps > { - /** - * The ref object to associated with this dragged itom. If this is not specified it will be - * returned in the `ref` field of the result object. - */ - ref?: RefObject - /** * A plain javascript item describing the data being dragged. * This is the only information available to the drop targets about the drag @@ -32,11 +25,6 @@ export interface DragSourceHookSpec< */ options?: DragSourceOptions - /** - * An optional dragPreview - */ - preview?: RefObject | Element - /** * DragPreview options */ @@ -45,7 +33,7 @@ export interface DragSourceHookSpec< /** * When the dragging starts, beginDrag is called. If an object is returned from this function it will overide the default dragItem */ - begin?: (monitor: DragSourceMonitor) => DragObject | undefined + begin?: (monitor: DragSourceMonitor) => DragObject | undefined | void /** * Optional. @@ -87,12 +75,6 @@ export interface DragSourceHookSpec< * Interface for the DropTarget specification object */ export interface DropTargetHookSpec { - /** - * The ref object to associated with this dragged itom. If this is not specified it will be - * returned in the `ref` field of the result object. - */ - ref?: RefObject - /** * The kinds of dragItems this dropTarget accepts */ diff --git a/packages/react-dnd/src/interfaces/monitors.ts b/packages/react-dnd/src/interfaces/monitors.ts index 0a05578e8c..bb035426c8 100644 --- a/packages/react-dnd/src/interfaces/monitors.ts +++ b/packages/react-dnd/src/interfaces/monitors.ts @@ -1,11 +1,11 @@ -import { Identifier, XYCoord } from 'dnd-core' +import { Identifier, XYCoord, Unsubscribe } from 'dnd-core' export interface HandlerManager { receiveHandlerId: (handlerId: Identifier | null) => void getHandlerId: () => Identifier | null } -export interface DragSourceMonitor extends HandlerManager { +export interface DragSourceMonitor extends HandlerManager, MonitorEventEmitter { /** * Returns true if no drag operation is in progress, and the owner's canDrag() returns true or is not defined. */ @@ -69,7 +69,14 @@ export interface DragSourceMonitor extends HandlerManager { getSourceClientOffset(): XYCoord | null } -export interface DropTargetMonitor extends HandlerManager { +export interface MonitorEventEmitter { + subscribeToStateChange( + fn: () => void, + options?: { handlerIds?: Identifier[] }, + ): Unsubscribe +} + +export interface DropTargetMonitor extends HandlerManager, MonitorEventEmitter { /** * Returns true if there is a drag operation in progress, and the owner's canDrop() returns true or is not defined. */ diff --git a/packages/react-dnd/src/registerSource.ts b/packages/react-dnd/src/registerSource.ts index b1f6269c75..53c7bb20c9 100644 --- a/packages/react-dnd/src/registerSource.ts +++ b/packages/react-dnd/src/registerSource.ts @@ -10,15 +10,9 @@ export default function registerSource( type: SourceType, source: DragSource, manager: DragDropManager, -): { - handlerId: Identifier - unregister: Unsubscribe -} { +): [Identifier, Unsubscribe] { const registry = manager.getRegistry() const sourceId = registry.addSource(type, source) - return { - handlerId: sourceId, - unregister: () => registry.removeSource(sourceId), - } + return [sourceId, () => registry.removeSource(sourceId)] } diff --git a/packages/react-dnd/src/registerTarget.ts b/packages/react-dnd/src/registerTarget.ts index 0e054e6ca7..7f80eb8e1b 100644 --- a/packages/react-dnd/src/registerTarget.ts +++ b/packages/react-dnd/src/registerTarget.ts @@ -10,12 +10,9 @@ export default function registerTarget( type: TargetType, target: DropTarget, manager: DragDropManager, -): { handlerId: Identifier; unregister: Unsubscribe } { +): [Identifier, Unsubscribe] { const registry = manager.getRegistry() const targetId = registry.addTarget(type, target) - return { - handlerId: targetId, - unregister: () => registry.removeTarget(targetId), - } + return [targetId, () => registry.removeTarget(targetId)] } diff --git a/packages/react-dnd/src/hooks/util.ts b/packages/react-dnd/src/utils/isRef.ts similarity index 100% rename from packages/react-dnd/src/hooks/util.ts rename to packages/react-dnd/src/utils/isRef.ts diff --git a/packages/react-dnd/src/wrapConnectorHooks.ts b/packages/react-dnd/src/wrapConnectorHooks.ts index 5518ab26dd..de910521fa 100644 --- a/packages/react-dnd/src/wrapConnectorHooks.ts +++ b/packages/react-dnd/src/wrapConnectorHooks.ts @@ -1,4 +1,4 @@ -import { isValidElement } from 'react' +import { isValidElement, ReactElement } from 'react' import cloneWithRef from './utils/cloneWithRef' function throwIfCompositeComponentElement(element: React.ReactElement) { @@ -24,17 +24,19 @@ function wrapHookToRecognizeElement(hook: (node: any, options: any) => void) { if (!isValidElement(elementOrNode)) { const node = elementOrNode hook(node, options) - return undefined + // return the node so it can be chained (e.g. when within callback refs + //
connectDragSource(connectDropTarget(node))}/> + return node } // If passed a ReactElement, clone it and attach this function as a ref. // This helps us achieve a neat API where user doesn't even know that refs // are being used under the hood. - const element = elementOrNode + const element: ReactElement | null = elementOrNode throwIfCompositeComponentElement(element as any) // When no options are passed, use the hook directly - const ref = options ? (node: any) => hook(node, options) : hook + const ref = options ? (node: Element) => hook(node, options) : hook return cloneWithRef(element, ref) } } diff --git a/tsconfig.json b/tsconfig.json index 0ced16122b..92b51498d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "noEmit": true, "baseUrl": ".", + "allowSyntheticDefaultImports": true, "paths": { "dnd-core": ["packages/dnd-core/src/index"], "dnd-core/*": ["packages/dnd-core/src/*"], diff --git a/yarn.lock b/yarn.lock index 67344613a6..88a48187d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3464,11 +3464,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3 escape-string-regexp "^1.0.5" supports-color "^5.3.0" -change-emitter@^0.1.2: - version "0.1.6" - resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" - integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU= - character-entities-html4@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.2.tgz#c44fdde3ce66b52e8d321d6c1bf46101f0150610" @@ -5891,7 +5886,7 @@ fbjs-css-vars@^1.0.0: resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== -fbjs@^0.8.0, fbjs@^0.8.1: +fbjs@^0.8.0: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= @@ -7411,11 +7406,6 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== -hoist-non-react-statics@^2.3.1: - version "2.5.5" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" - integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== - hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" @@ -12219,7 +12209,7 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== -react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== @@ -12427,18 +12417,6 @@ realpath-native@^1.0.0, realpath-native@^1.0.2: dependencies: util.promisify "^1.0.0" -recompose@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0" - integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w== - dependencies: - "@babel/runtime" "^7.0.0" - change-emitter "^0.1.2" - fbjs "^0.8.1" - hoist-non-react-statics "^2.3.1" - react-lifecycles-compat "^3.0.2" - symbol-observable "^1.0.4" - recursive-readdir@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" @@ -14034,7 +14012,7 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: +symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==