Skip to content

Commit

Permalink
Less hackish solution for dynamic items (#48)
Browse files Browse the repository at this point in the history
* Add dynamic prop and don't clobber item keys.

Solves #46 and other potential issues.

* Docs for dynamic prop.

* Keep giving items keys the same way as before if non-dynamic.

* Actually, just clobber the key if it's not defined.

* Undo a stupid hack and fix the demo instead.
  • Loading branch information
antialiasis authored and jasonslyvia committed May 17, 2016
1 parent cb7a0cf commit 9572407
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 29 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ Constrain dragging area within sortable container.

[demo](http://jasonslyvia.github.io/react-anything-sortable/demo/index.html#/containment)

### dynamic

Type: Bool Default: false

Dynamically update the sortable when its children change. If using this option, make sure to use the onSort callback to update the order of the children passed to the Sortable component when the user sorts!

[demo](http://jasonslyvia.github.io/react-anything-sortable/demo/index.html#/dynamic)

### sortHandle

Type: String Default: undefined
Expand All @@ -139,7 +147,7 @@ Will be returned by `onSort` callback in the form of array.

1. Specify your style for `Sortable` and `Sortable Items`, check `sortable.css`, **it is NOT optional!**
2. Don't forget the `this.renderWithSortable` call in `SortableItem`, or spread props to your component if using decorators.
3. Since we can't track any children modification in `Sortable`, you have to use `key` to force update `Sortable` when adding/removing children. Checkout [dynamic demo](http://jasonslyvia.github.io/react-anything-sortable/demo/#/dynamic) for more details.
3. In order to dynamically add or remove `SortableItem`s or change their order from outside the `Sortable`, you must use the `dynamic` option. This also requires using the `onSort` callback to update the order of the children when sorting happens.
4. Make sure to add `draggable={false}` to images within sortable components to prevent glitching. See [here](https://github.com/jasonslyvia/react-anything-sortable/blob/master/demo/components/ImageItem.js) for an example.


Expand Down
10 changes: 3 additions & 7 deletions demo/pages/dynamic.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ export default class Dynamic extends React.Component {
this.state = {
arr: [998, 225, 13]
};
this._sortableKey = 0;
}

handleSort(sortedArray) {
this._sortableKey++;
this.setState({
arr: sortedArray
});
}

handleAddElement() {
this._sortableKey++;
this.setState({
arr: this.state.arr.concat(Math.round(Math.random() * 1000))
});
Expand All @@ -28,7 +25,6 @@ export default class Dynamic extends React.Component {
handleRemoveElement(index) {
const newArr = this.state.arr.slice();
newArr.splice(index, 1);
this._sortableKey++;

this.setState({
arr: newArr
Expand All @@ -38,10 +34,10 @@ export default class Dynamic extends React.Component {
render() {
function renderItem(num, index) {
return (
<DemoItem key={index} className="dynamic-item" sortData={num}>
<DemoItem key={num} className="dynamic-item" sortData={num}>
{num}
<span className="delete"
onMouseDown={this.handleRemoveElement.bind(this, index)}
onClick={this.handleRemoveElement.bind(this, index)}
>&times;</span>
</DemoItem>
);
Expand All @@ -55,7 +51,7 @@ export default class Dynamic extends React.Component {
</h4>
<div className="dynamic-demo">
<button onClick={::this.handleAddElement}>Add 1 element</button>
<Sortable key={this._sortableKey} onSort={::this.handleSort}>
<Sortable onSort={::this.handleSort} dynamic>
{this.state.arr.map(renderItem, this)}
</Sortable>
</div>
Expand Down
6 changes: 3 additions & 3 deletions demo/pages/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export default class HOC extends React.Component {
<p className="sort-result">{this.state.result}</p>
<Sortable onSort={::this.handleSort}>
<ImageItem src="http://ww4.sinaimg.cn/large/831e9385gw1equsc4s1hbj207y02xmx9.jpg"
sortData="react" />
sortData="react" key={1} />
<ImageItem src="http://ww4.sinaimg.cn/large/831e9385gw1equsc3q8lej20fz04waa8.jpg"
sortData="angular" />
sortData="angular" key={2} />
<ImageItem src="http://ww4.sinaimg.cn/large/831e9385gw1equsc46m7zj20ff02zq3h.jpg"
sortData="backbone" />
sortData="backbone" key={3} />
</Sortable>
</div>
);
Expand Down
34 changes: 26 additions & 8 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,12 @@ return /******/ (function(modules) { // webpackBootstrap
className: _react.PropTypes.string,
sortHandle: _react.PropTypes.string,
containment: _react.PropTypes.bool,
dynamic: _react.PropTypes.bool,
children: _react.PropTypes.arrayOf(_react.PropTypes.node)
},

getInitialState: function getInitialState() {
var children = Array.isArray(this.props.children) ? this.props.children : [this.props.children];
setArrays: function setArrays(currentChildren) {
var children = Array.isArray(currentChildren) ? currentChildren : [currentChildren];

var sortChildren = children.filter(getSortTarget);
this.sortChildren = sortChildren;
Expand All @@ -126,6 +127,10 @@ return /******/ (function(modules) { // webpackBootstrap
while (i < this._dimensionArr.length) {
this._orderArr.push(i++);
}
},

getInitialState: function getInitialState() {
this.setArrays(this.props.children);

return {
isDragging: false,
Expand All @@ -145,6 +150,16 @@ return /******/ (function(modules) { // webpackBootstrap
this._right = this._left + rect.width;
},

componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var _props = this.props;
var children = _props.children;
var dynamic = _props.dynamic;

if (dynamic && nextProps.children !== children) {
this.setArrays(nextProps.children);
}
},

componentWillUnmount: function componentWillUnmount() {
this.unbindEvent();
},
Expand Down Expand Up @@ -493,16 +508,21 @@ return /******/ (function(modules) { // webpackBootstrap
var isPlaceHolder = _dimensionArr[index].isPlaceHolder;
var itemClassName = 'ui-sortable-item\n ' + (isPlaceHolder && 'ui-sortable-placeholder') + '\n ' + (_this3.state.isDragging && isPlaceHolder && 'visible');

return _react2['default'].cloneElement(item, {
key: index,
var sortableProps = {
sortableClassName: item.props.className + ' ' + itemClassName,
sortableIndex: index,
onSortableItemReadyToMove: isPlaceHolder ? undefined : function (e) {
_this3.handleMouseDown.call(_this3, e, index);
},
onSortableItemMount: _this3.handleChildUpdate,
sortHandle: _this3.props.sortHandle
});
};

if (item.key === undefined) {
sortableProps.key = index;
}

return _react2['default'].cloneElement(item, sortableProps);
});

var children = Array.isArray(this.props.children) ? this.props.children : [this.props.children];
Expand Down Expand Up @@ -534,7 +554,7 @@ return /******/ (function(modules) { // webpackBootstrap
};
return _react2['default'].cloneElement(item, {
sortableClassName: item.props.className + ' ui-sortable-item ui-sortable-dragging',
key: this._dimensionArr.length,
key: '_dragging',
sortableStyle: style,
isDragging: true,
sortHandle: this.props.sortHandle
Expand Down Expand Up @@ -856,7 +876,6 @@ return /******/ (function(modules) { // webpackBootstrap
sortable: true,
className: sortableClassName,
style: sortableStyle,
key: sortableIndex,
sortHandle: sortHandle,
onMouseDown: this.handleSortableItemReadyToMove.bind(this),
onTouchStart: this.handleSortableItemReadyToMove.bind(this)
Expand Down Expand Up @@ -904,7 +923,6 @@ return /******/ (function(modules) { // webpackBootstrap
return _react2['default'].cloneElement(item, {
className: this.props.sortableClassName,
style: this.props.sortableStyle,
key: this.props.sortableIndex,
sortHandle: this.props.sortHandle,
onMouseDown: this.handleSortableItemReadyToMove,
onTouchStart: this.handleSortableItemReadyToMove
Expand Down
2 changes: 0 additions & 2 deletions src/SortableItemMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export default (Component) => {
sortable={true}
className={sortableClassName}
style={sortableStyle}
key={sortableIndex}
sortHandle={sortHandle}
onMouseDown={::this.handleSortableItemReadyToMove}
onTouchStart={::this.handleSortableItemReadyToMove}
Expand Down Expand Up @@ -133,7 +132,6 @@ export default (Component) => {
return React.cloneElement(item, {
className: this.props.sortableClassName,
style: this.props.sortableStyle,
key: this.props.sortableIndex,
sortHandle: this.props.sortHandle,
onMouseDown: this.handleSortableItemReadyToMove,
onTouchStart: this.handleSortableItemReadyToMove
Expand Down
33 changes: 25 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ const Sortable = React.createClass({
className: PropTypes.string,
sortHandle: PropTypes.string,
containment: PropTypes.bool,
dynamic: PropTypes.bool,
children: PropTypes.arrayOf(PropTypes.node)
},

getInitialState() {
const children = Array.isArray(this.props.children) ?
this.props.children :
[this.props.children];
setArrays(currentChildren) {
const children = Array.isArray(currentChildren) ?
currentChildren :
[currentChildren];

const sortChildren = children.filter(getSortTarget);
this.sortChildren = sortChildren;
Expand All @@ -55,6 +56,10 @@ const Sortable = React.createClass({
while (i < this._dimensionArr.length) {
this._orderArr.push(i++);
}
},

getInitialState() {
this.setArrays(this.props.children);

return {
isDragging: false,
Expand All @@ -74,6 +79,13 @@ const Sortable = React.createClass({
this._right = this._left + rect.width;
},

componentWillReceiveProps(nextProps) {
const { children, dynamic } = this.props;
if (dynamic && nextProps.children !== children) {
this.setArrays(nextProps.children);
}
},

componentWillUnmount() {
this.unbindEvent();
},
Expand Down Expand Up @@ -424,16 +436,21 @@ const Sortable = React.createClass({
${isPlaceHolder && 'ui-sortable-placeholder'}
${this.state.isDragging && isPlaceHolder && 'visible'}`;

return React.cloneElement(item, {
key: index,
const sortableProps = {
sortableClassName: `${item.props.className} ${itemClassName}`,
sortableIndex: index,
onSortableItemReadyToMove: isPlaceHolder ? undefined : (e) => {
this.handleMouseDown.call(this, e, index);
},
onSortableItemMount: this.handleChildUpdate,
sortHandle: this.props.sortHandle
});
};

if (item.key === undefined) {
sortableProps.key = index;
}

return React.cloneElement(item, sortableProps);
});

const children = Array.isArray(this.props.children) ?
Expand Down Expand Up @@ -467,7 +484,7 @@ const Sortable = React.createClass({
};
return React.cloneElement(item, {
sortableClassName: `${item.props.className} ui-sortable-item ui-sortable-dragging`,
key: this._dimensionArr.length,
key: '_dragging',
sortableStyle: style,
isDragging: true,
sortHandle: this.props.sortHandle
Expand Down
67 changes: 67 additions & 0 deletions test/specs/sortable-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,5 +557,72 @@ describe('Sortable', () => {
expect(draggingItem).to.exist;
});
});

describe('Dynamic sortables', () => {
afterEach(() => {
ReactDOM.unmountComponentAtNode(document.getElementById('react'));
});

it('should automatically update when children change', () => {
ReactDOM.render(
<Sortable dynamic>
<DemoItem sortData="1" className="item-1" key={1}>1</DemoItem>
<DemoItem sortData="2" className="item-2" key={2}>2</DemoItem>
</Sortable>
, document.getElementById('react'));

ReactDOM.render(
<Sortable dynamic>
<DemoItem sortData="2" className="item-2" key={2}>2</DemoItem>
<DemoItem sortData="1" className="item-1" key={1}>1</DemoItem>
<DemoItem sortData="3" className="item-3" key={3}>3</DemoItem>
</Sortable>
, document.getElementById('react'));

let children = document.querySelectorAll('.ui-sortable-item');
expect(children.length).to.equal(3);
expect(children[0].textContent).to.equal('2');

ReactDOM.render(
<Sortable dynamic>
<DemoItem sortData="2" className="item-2" key={2}>2</DemoItem>
<DemoItem sortData="3" className="item-3" key={3}>3</DemoItem>
</Sortable>
, document.getElementById('react'));

children = document.querySelectorAll('.ui-sortable-item');
expect(children.length).to.equal(2);
expect(children[1].textContent).to.equal('3');
});

it('should correctly reorder keyed items, maintaining state', () => {
ReactDOM.render(
<Sortable className="style-for-test" dynamic>
<DemoItem sortData="1" className="item-1" key={1}><input size="3" /></DemoItem>
<DemoItem sortData="2" className="item-2" key={2}><input size="3" /></DemoItem>
<DemoItem sortData="3" className="item-3" key={3}><input size="3" /></DemoItem>
</Sortable>
, document.getElementById('react'));

let inputs = document.querySelectorAll('.ui-sortable-item input');
inputs[0].value = 'foo';
inputs[1].value = 'bar';
inputs[2].value = 'baz';

ReactDOM.render(
<Sortable className="style-for-test" dynamic>
<DemoItem sortData="2" className="item-2" key={2}><input size="3" /></DemoItem>
<DemoItem sortData="3" className="item-3" key={3}><input size="3" /></DemoItem>
<DemoItem sortData="1" className="item-1" key={1}><input size="3" /></DemoItem>
</Sortable>
, document.getElementById('react'));

inputs = document.querySelectorAll('.ui-sortable-item input');

expect(inputs[0].value).to.equal('bar');
expect(inputs[1].value).to.equal('baz');
expect(inputs[2].value).to.equal('foo');
});
});
});

0 comments on commit 9572407

Please sign in to comment.