├── .gitignore
├── app
├── styles
│ └── application.styl
├── redux
│ ├── reducer.js
│ └── store.js
├── routes.js
├── containers
│ ├── App.js
│ └── Index.js
├── index.js
└── components
│ ├── Tree.js
│ └── Item.js
├── README.md
├── index.html
├── .babelrc
├── devServer.js
├── package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | public/index.html
4 | public/js
5 |
--------------------------------------------------------------------------------
/app/styles/application.styl:
--------------------------------------------------------------------------------
1 | body
2 | font-family: 'Helvetica', sans-serif
3 | line-height: 1.4
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Nested list D&D example
2 |
3 | To run it in your computer:
4 |
5 | ```bash
6 | npm i
7 | npm start
8 | ```
9 |
10 | [Demo](http://tamagokun.github.io/example-react-dnd-nested/)
11 |
--------------------------------------------------------------------------------
/app/redux/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { routeReducer as routing } from 'redux-simple-router'
3 | import { reducer as form } from 'redux-form'
4 |
5 | export default combineReducers({
6 | routing,
7 | form
8 | })
9 |
--------------------------------------------------------------------------------
/app/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import reducer from './reducer'
4 |
5 | export default () => {
6 | return compose(
7 | applyMiddleware(
8 | thunk
9 | ),
10 | )(createStore)(reducer)
11 | }
12 |
--------------------------------------------------------------------------------
/app/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, IndexRoute } from 'react-router'
3 |
4 | import App from './containers/App'
5 | import Index from './containers/Index'
6 |
7 | export default (store) => {
8 | return
9 |
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/app/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { DragDropContext } from 'react-dnd'
3 | import HTML5Backend from 'react-dnd-html5-backend'
4 |
5 | @DragDropContext(HTML5Backend)
6 | export default class App extends Component {
7 | render() {
8 | return
9 | {this.props.children}
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Example
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-0"],
3 | "plugins": ["transform-decorators-legacy"],
4 | "env": {
5 | "development": {
6 | "plugins": [
7 | ["react-transform", {
8 | "transforms": [{
9 | "transform": "react-transform-hmr",
10 | "imports": ["react"],
11 | "locals": ["module"]
12 | }, {
13 | "transform": "react-transform-catch-errors",
14 | "imports": ["react", "redbox-react"]
15 | }]
16 | }]
17 | ]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | import './styles/application.styl'
2 |
3 | import React, { Component, PropTypes } from 'react'
4 | import { render } from 'react-dom'
5 | import { Provider } from 'react-redux'
6 | import { Router } from 'react-router'
7 | import { createHistory } from 'history'
8 | import { syncReduxAndRouter } from 'redux-simple-router'
9 |
10 | import createStore from './redux/store'
11 | import createRoutes from './routes'
12 |
13 | const store = createStore()
14 | const history = createHistory()
15 | const routes = createRoutes(store)
16 | syncReduxAndRouter(history, store)
17 |
18 | render(
19 |
20 |
21 | {routes}
22 |
23 | ,
24 | document.getElementById('app')
25 | )
26 |
--------------------------------------------------------------------------------
/devServer.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var express = require('express')
3 | var webpack = require('webpack')
4 | var config = require('./webpack.config')
5 |
6 | var app = express()
7 | var compiler = webpack(config)
8 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
9 | noInfo: true,
10 | publicPath: config.output.publicPath
11 | })
12 |
13 | app.use(express.static('public'))
14 | app.use(devMiddleware)
15 | app.use(require('webpack-hot-middleware')(compiler))
16 |
17 | app.get('*', function(req, res) {
18 | if (req.accepts('html')) {
19 | var indexPath = path.join(config.output.path, 'index.html')
20 | var index = devMiddleware.fileSystem.readFileSync(indexPath)
21 | res.set('Content-Type', 'text/html')
22 | res.send(index)
23 | }
24 | })
25 |
26 | app.listen(3000, 'localhost', function(err) {
27 | if (err) {
28 | console.log(err)
29 | return
30 | }
31 |
32 | console.log('Listening at http://localhost:3000')
33 | })
34 |
--------------------------------------------------------------------------------
/app/components/Tree.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { DropTarget } from 'react-dnd'
3 | import Item from './Item'
4 |
5 | const target = {
6 | drop() {},
7 |
8 | hover(props, monitor) {
9 | const {id: draggedId, parent, items} = monitor.getItem()
10 |
11 | if (!monitor.isOver({shallow: true})) return
12 |
13 | const descendantNode = props.find(props.parent, items)
14 | if (descendantNode) return
15 | if (parent == props.parent || draggedId == props.parent) return
16 |
17 | props.move(draggedId, props.id, props.parent)
18 | }
19 | }
20 |
21 | @DropTarget('ITEM', target, (connect, monitor) => ({
22 | connectDropTarget: connect.dropTarget()
23 | }))
24 | export default class Tree extends Component {
25 | static propTypes = {
26 | items : PropTypes.array.isRequired,
27 | parent : PropTypes.any,
28 | move : PropTypes.func.isRequired,
29 | find : PropTypes.func.isRequired
30 | };
31 |
32 | render() {
33 | const {connectDropTarget, items, parent, move, find} = this.props
34 |
35 | return connectDropTarget(
36 |
43 | {items.map((item, i) => {
44 | return
52 | })}
53 |
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "mike@ripeworks.com",
3 | "name": "example-react-dnd-nested",
4 | "description": "",
5 | "version": "0.0.1",
6 | "repository": {
7 | "type": "git",
8 | "url": ""
9 | },
10 | "scripts": {
11 | "start": "node devServer",
12 | "build": "webpack -p"
13 | },
14 | "devDependencies": {
15 | "babel-core": "^6.3.17",
16 | "babel-loader": "^6.2.0",
17 | "babel-plugin-react-transform": "^2.0.0-beta1",
18 | "babel-plugin-transform-decorators-legacy": "^1.2.0",
19 | "babel-preset-es2015": "^6.3.13",
20 | "babel-preset-react": "^6.3.13",
21 | "babel-preset-stage-0": "^6.3.13",
22 | "css-loader": "^0.15.6",
23 | "express": "^4.13.3",
24 | "file-loader": "^0.8.4",
25 | "html-webpack-plugin": "^1.7.0",
26 | "nib": "^1.1.0",
27 | "react-transform-catch-errors": "^1.0.0",
28 | "react-transform-hmr": "^1.0.1",
29 | "redbox-react": "^1.2.0",
30 | "redux-devtools": "^2.1.0",
31 | "script-loader": "^0.6.1",
32 | "style-loader": "^0.12.3",
33 | "stylus-loader": "^1.2.1",
34 | "url-loader": "^0.5.6",
35 | "webpack": "^1.12.9",
36 | "webpack-dev-middleware": "^1.4.0",
37 | "webpack-hot-middleware": "^2.6.0"
38 | },
39 | "dependencies": {
40 | "classnames": "^2.1.3",
41 | "history": "1.13.1",
42 | "react": "^0.14.3",
43 | "react-dnd": "^2.0.2",
44 | "react-dnd-html5-backend": "^2.0.2",
45 | "react-dom": "^0.14.3",
46 | "react-redux": "^4.0.0",
47 | "react-router": "^1.0.2",
48 | "redux": "^3.0.5",
49 | "redux-action-types": "^1.0.1",
50 | "redux-form": "^4.0.3",
51 | "redux-simple-router": "^1.0.0",
52 | "redux-thunk": "^1.0.2"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/components/Item.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { DragSource, DropTarget } from 'react-dnd'
3 | import Tree from './Tree'
4 |
5 | const source = {
6 | beginDrag(props) {
7 | return {
8 | id: props.id,
9 | parent: props.parent,
10 | items: props.item.children
11 | }
12 | },
13 |
14 | isDragging(props, monitor) {
15 | return props.id == monitor.getItem().id
16 | }
17 | }
18 |
19 | const target = {
20 | canDrop() {
21 | return false
22 | },
23 |
24 | hover(props, monitor) {
25 | const {id: draggedId} = monitor.getItem()
26 | const {id: overId} = props
27 |
28 | if (draggedId == overId || draggedId == props.parent) return
29 | if (!monitor.isOver({shallow: true})) return
30 |
31 | props.move(draggedId, overId, props.parent)
32 | }
33 | }
34 |
35 | @DropTarget('ITEM', target, connect => ({
36 | connectDropTarget: connect.dropTarget()
37 | }))
38 | @DragSource('ITEM', source, (connect, monitor) => ({
39 | connectDragSource: connect.dragSource(),
40 | connectDragPreview: connect.dragPreview(),
41 | isDragging: monitor.isDragging()
42 | }))
43 | export default class Item extends Component {
44 | static propTypes = {
45 | id : PropTypes.any.isRequired,
46 | parent : PropTypes.any,
47 | item : PropTypes.object,
48 | move : PropTypes.func,
49 | find : PropTypes.func
50 | };
51 |
52 | render() {
53 | const {
54 | connectDropTarget, connectDragPreview, connectDragSource,
55 | item: {id, title, children}, parent, move, find
56 | } = this.props
57 |
58 | return connectDropTarget(connectDragPreview(
59 |
60 | {connectDragSource(
61 |
{title}
68 | )}
69 |
75 |
76 | ))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var nib = require('nib')
2 | var webpack = require('webpack')
3 | var HtmlWebpackPlugin = require('html-webpack-plugin')
4 |
5 | var env = process.env.NODE_ENV || 'development'
6 |
7 | var config = {
8 | path: __dirname + '/public',
9 | entry: [
10 | './app/index.js'
11 | ],
12 | plugins: [
13 | new webpack.NoErrorsPlugin(),
14 | new webpack.optimize.OccurenceOrderPlugin(),
15 | new webpack.DefinePlugin({
16 | "process.env": {
17 | NODE_ENV: JSON.stringify(env)
18 | }
19 | }),
20 | new webpack.optimize.UglifyJsPlugin({
21 | compressor: {
22 | warnings: false
23 | }
24 | }),
25 | new HtmlWebpackPlugin({
26 | template: './index.html',
27 | hash: true,
28 | inject: 'body'
29 | })
30 | ],
31 | devtool: 'source-map'
32 | }
33 |
34 | if (env == 'development') {
35 | config.entry.unshift('webpack-hot-middleware/client')
36 | config.plugins = [
37 | new webpack.HotModuleReplacementPlugin(),
38 | new webpack.NoErrorsPlugin(),
39 | new webpack.DefinePlugin({"process.env": { NODE_ENV: JSON.stringify(env) }}),
40 | new HtmlWebpackPlugin({
41 | template: './index.html',
42 | hash: true,
43 | inject: 'body'
44 | })
45 | ]
46 | config.devtool = 'cheap-module-eval-source-map'
47 | }
48 |
49 | module.exports = {
50 | entry: config.entry,
51 | output: {
52 | path: config.path,
53 | filename: 'js/app.js',
54 | cssFilename: 'css/application.css',
55 | publicPath: '/',
56 | },
57 | module: {
58 | loaders: [
59 | { test: /\.js$/, loader: 'babel', exclude: /node_modules/},
60 | { test: /\.styl$/, loader: 'style-loader!css-loader!stylus-loader' },
61 | { test: /\.css$/, loader: 'style-loader!css-loader'},
62 | { test: /\.(otf|eot|svg|ttf|woff)/, loader: 'url-loader' },
63 | { test: /\.(jpg|png|gif|json)/, loader: 'url-loader' },
64 | ]
65 | },
66 | stylus: {
67 | 'include css': true,
68 | use: [nib()],
69 | },
70 | resolve: {
71 | extensions: ['', '.js', '.json'],
72 | },
73 | plugins: config.plugins,
74 | devtool: config.devtool
75 | }
76 |
--------------------------------------------------------------------------------
/app/containers/Index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Tree from '../components/Tree'
3 |
4 | export default class Index extends Component {
5 | state = {
6 | tree: [
7 | {
8 | id: 1, title: 'Tatooine',
9 | children: [
10 | {id: 2, title: 'Endor', children: []},
11 | {id: 3, title: 'Hoth', children: []},
12 | {id: 4, title: 'Dagobah', children: []},
13 | ]
14 | },
15 | {
16 | id: 5, title: 'Death Star',
17 | children: []
18 | },
19 | {
20 | id: 6, title: 'Alderaan',
21 | children: [
22 | {
23 | id: 7, title: 'Bespin',
24 | children: [
25 | {id: 8, title: 'Jakku', children: []}
26 | ]
27 | }
28 | ]
29 | }
30 | ]
31 | };
32 |
33 | moveItem(id, afterId, nodeId) {
34 | if (id == afterId) return
35 |
36 | let {tree} = this.state
37 |
38 | const removeNode = (id, items) => {
39 | for (const node of items) {
40 | if (node.id == id) {
41 | items.splice(items.indexOf(node), 1)
42 | return
43 | }
44 |
45 | if (node.children && node.children.length) {
46 | removeNode(id, node.children)
47 | }
48 | }
49 | }
50 |
51 | const item = {...this.findItem(id, tree)}
52 | if (!item.id) {
53 | return
54 | }
55 |
56 | const dest = nodeId ? this.findItem(nodeId, tree).children : tree
57 |
58 | if (!afterId) {
59 | removeNode(id, tree)
60 | dest.push(item)
61 | } else {
62 | const index = dest.indexOf(dest.filter(v => v.id == afterId).shift())
63 | removeNode(id, tree)
64 | dest.splice(index, 0, item)
65 | }
66 |
67 | this.setState({tree})
68 | }
69 |
70 | findItem(id, items) {
71 | for (const node of items) {
72 | if (node.id == id) return node
73 | if (node.children && node.children.length) {
74 | const result = this.findItem(id, node.children)
75 | if (result) {
76 | return result
77 | }
78 | }
79 | }
80 |
81 | return false
82 | }
83 |
84 | render() {
85 | const {tree} = this.state
86 |
87 | return
88 |
94 |
95 | }
96 | }
97 |
--------------------------------------------------------------------------------