1 | # `react-sortablejs`
|
2 |
|
3 | React bindings to [SortableJS](https://github.com/SortableJS/Sortable)
|
4 |
|
5 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
|
6 |
|
7 | Please note that this is not considered ready for production, as there are still a number of bugs being sent through.
|
8 |
|
9 | ## Features
|
10 |
|
11 | ## Installation
|
12 |
|
13 | `sortablejs` and `@types/sortablejs` are peer dependencies. The latter only used if intellisense/typescript is desired.
|
14 |
|
15 | ```shell
|
16 | npm install --save react-sortablejs sortablejs
|
17 | npm install --save-dev @types/sortablejs
|
18 |
|
19 | # OR
|
20 | yarn add react-sortablejs sortablejs
|
21 | yarn add -D @types/sortablejs
|
22 | ```
|
23 |
|
24 | ## Learn
|
25 |
|
26 | Here is the TLDR of what sortable is:
|
27 |
|
28 | ```md
|
29 | - Shopping List: # list of items / sortable. This represents `react-sortablejs`
|
30 | - eggs # list item. These are all the items in the list and is what you move around.
|
31 | - bread # list item
|
32 | - milk # list item
|
33 | ```
|
34 |
|
35 | ## Usage/Examples
|
36 |
|
37 | ### Function Component
|
38 |
|
39 | ```tsx
|
40 | import React, { FC, useState } from "react";
|
41 | import { ReactSortable } from "react-sortablejs";
|
42 |
|
43 | interface ItemType {
|
44 | id: number;
|
45 | name: string;
|
46 | }
|
47 |
|
48 | export const BasicFunction: FC = (props) => {
|
49 | const [state, setState] = useState<ItemType[]>([
|
50 | { id: 1, name: "shrek" },
|
51 | { id: 2, name: "fiona" },
|
52 | ]);
|
53 |
|
54 | return (
|
55 | <ReactSortable list={state} setList={setState}>
|
56 | {state.map((item) => (
|
57 | <div key={item.id}>{item.name}</div>
|
58 | ))}
|
59 | </ReactSortable>
|
60 | );
|
61 | };
|
62 | ```
|
63 |
|
64 | ### Class Component
|
65 |
|
66 | ```tsx
|
67 | import React, { Component } from "react";
|
68 | import { ReactSortable } from "react-sortablejs";
|
69 |
|
70 | interface BasicClassState {
|
71 | list: { id: string; name: string }[];
|
72 | }
|
73 |
|
74 | export class BasicClass extends Component<{}, BasicClassState> {
|
75 | state: BasicClassState = {
|
76 | list: [{ id: "1", name: "shrek" }],
|
77 | };
|
78 | render() {
|
79 | return (
|
80 | <ReactSortable
|
81 | list={this.state.list}
|
82 | setList={(newState) => this.setState({ list: newState })}
|
83 | >
|
84 | {this.state.list.map((item) => (
|
85 | <div key={item.id}>{item.name}</div>
|
86 | ))}
|
87 | </ReactSortable>
|
88 | );
|
89 | }
|
90 | }
|
91 | ```
|
92 |
|
93 | ## Plugins
|
94 |
|
95 | Sortable has some pretty cool plugins such as MultiDrag and Swap.
|
96 |
|
97 | By Default:
|
98 |
|
99 | - AutoScroll is premounted and enabled.
|
100 | - OnSpill is premounted and NOT enabled.
|
101 | - MultiDrag and Swap and NOT premounted and NOT enabled
|
102 |
|
103 | You must mount the plugin with sortable **ONCE ONLY**.
|
104 |
|
105 | ```tsx
|
106 | import React from "react";
|
107 | import { ReactSortable, Sortable, MultiDrag, Swap } from "react-sortablejs";
|
108 |
|
109 | // mount whatever plugins you'd like to. These are the only current options.
|
110 | Sortable.mount(new MultiDrag(), new Swap());
|
111 |
|
112 | const App = () => {
|
113 | const [state, setState] = useState([
|
114 | { id: 1, name: "shrek" },
|
115 | { id: 2, name: "fiona" },
|
116 | ]);
|
117 |
|
118 | return (
|
119 | <ReactSortable
|
120 | multiDrag // enables mutidrag
|
121 | // OR
|
122 | swap // enables swap
|
123 | >
|
124 | {state.map((item) => (
|
125 | <div key={item.id}>{item.name}</div>
|
126 | ))}
|
127 | </ReactSortable>
|
128 | );
|
129 | };
|
130 | ```
|
131 |
|
132 | ## Sortable API
|
133 |
|
134 | For a comprehensive list of options, please visit https://github.com/SortableJS/Sortable#options.
|
135 |
|
136 | Those options are applied as follows.
|
137 |
|
138 | ```tsx
|
139 | Sortable.create(element, {
|
140 | group: " groupName",
|
141 | animation: 200,
|
142 | delayOnTouchStart: true,
|
143 | delay: 2,
|
144 | });
|
145 |
|
146 | // --------------------------
|
147 | // Will now be..
|
148 | // --------------------------
|
149 |
|
150 | import React from "react";
|
151 | import { ReactSortable } from "react-sortablejs";
|
152 |
|
153 | const App = () => {
|
154 | const [state, setState] = useState([
|
155 | { id: 1, name: "shrek" },
|
156 | { id: 2, name: "fiona" },
|
157 | ]);
|
158 |
|
159 | return (
|
160 | <ReactSortable
|
161 | // here they are!
|
162 | group="groupName"
|
163 | animation={200}
|
164 | delayOnTouchStart={true}
|
165 | delay={2}
|
166 | >
|
167 | {state.map((item) => (
|
168 | <div key={item.id}>{item.name}</div>
|
169 | ))}
|
170 | </ReactSortable>
|
171 | );
|
172 | };
|
173 | ```
|
174 |
|
175 | ## React API
|
176 |
|
177 | ### id, className, style
|
178 |
|
179 | These are all defaults DOM attributes. Nothing special here.
|
180 |
|
181 | ### list
|
182 |
|
183 | The same as `state` in `const [ state, setState] = useState([{ id: 1}, {id: 2}])`
|
184 |
|
185 | `state` must be an array of items, with each item being an object that has the following shape:
|
186 |
|
187 | ```ts
|
188 | /** The unique id associated with your item. It's recommended this is the same as the key prop for your list item. */
|
189 | id: string | number;
|
190 | /** When true, the item is selected using MultiDrag */
|
191 | selected?: boolean;
|
192 | /** When true, the item is deemed "chosen", which basically just a mousedown event. */
|
193 | chosen?: boolean;
|
194 | /** When true, it will not be possible to pick this item up in the list. */
|
195 | filtered?: boolean;
|
196 | [property: string]: any;
|
197 | ```
|
198 |
|
199 | ### setList
|
200 |
|
201 | The same as `setState` in `const [ state, setState] = useState([{ id: 1}, {id: 2}])`
|
202 |
|
203 | ### clone
|
204 |
|
205 | If you're using `{group: { name: 'groupName', pull: 'clone'}}`, this means your in 'clone' mode. You should provide a function for this.
|
206 |
|
207 | Check out the source code of the clone example for more information. I'll write it here soon.
|
208 |
|
209 | ### tag
|
210 |
|
211 | ReactSortable is a `div` element by default. This can be changed to be any HTML element (for example `ul`, `ol`)
|
212 | or can be a React component.
|
213 |
|
214 | This value, be the component or the HTML element should be passed down under `props.tag`.
|
215 |
|
216 | Let's explore both here.
|
217 |
|
218 | #### HTML Element
|
219 |
|
220 | Here we will use a `ul`. You can use any HTML.
|
221 | Just add the string and ReactSortable will use a `li` instead of a `div`.
|
222 |
|
223 | ```tsx
|
224 | import React, { FC, useState } from "react";
|
225 | import { ReactSortable } from "react-sortablejs";
|
226 |
|
227 | export const BasicFunction: FC = (props) => {
|
228 | const [state, setState] = useState([{ id: "1", name: "shrek" }]);
|
229 |
|
230 | return (
|
231 | <ReactSortable tag="ul" list={state} setList={setState}>
|
232 | {state.map((item) => (
|
233 | <li key={item.id}>{item.name}</li>
|
234 | ))}
|
235 | </ReactSortable>
|
236 | );
|
237 | };
|
238 | ```
|
239 |
|
240 | #### Custom Component
|
241 |
|
242 | When using a custom component in the `tag` prop, the only component it allows is a `forwardRef` component.
|
243 | Currently we only support components who use the `React.forwardRef` API.
|
244 |
|
245 | If it doesn't have one, you can add one using `React.forwardRef()`.
|
246 |
|
247 | > todo: Some third party UI components may have nested elements to create the look they're after.
|
248 | > This could be an issue and not sure how to fix.
|
249 |
|
250 | ```tsx
|
251 | import React, { FC, useState, forwardRef } from "react";
|
252 | import { ReactSortable } from "react-sortablejs";
|
253 |
|
254 | // This is just like a normal component, but now has a ref.
|
255 | const CustomComponent = forwardRef<HTMLDivElement, any>((props, ref) => {
|
256 | return <div ref={ref}>{props.children}</div>;
|
257 | });
|
258 |
|
259 | export const BasicFunction: FC = (props) => {
|
260 | const [state, setState] = useState([
|
261 | { id: 1, name: "shrek" },
|
262 | { id: 2, name: "fiona" },
|
263 | ]);
|
264 |
|
265 | return (
|
266 | <ReactSortable tag={CustomComponent} list={state} setList={setState}>
|
267 | {state.map((item) => (
|
268 | <div key={item.id}>{item.name}</div>
|
269 | ))}
|
270 | </ReactSortable>
|
271 | );
|
272 | };
|
273 | ```
|
274 |
|
275 | ## How does it work?
|
276 |
|
277 | Sortable affects the DOM, adding, and removing nodes/css when it needs to in order to achieve the smooth transitions we all know an love.
|
278 | This component reverses many of it's actions of the DOM so React can handle this when the state changes.
|
279 |
|
280 | ## Caveats / Gotchas
|
281 |
|
282 | ### `key !== index`
|
283 |
|
284 | DO NOT use the index as a key for your list items. Sorting will not work.
|
285 |
|
286 | In all the examples above, I used an object with an ID. You should do the same!
|
287 |
|
288 | I may even enforce this into the design to eliminate errors.
|
289 |
|
290 | ### Nesting
|
291 |
|
292 | #### Problem
|
293 |
|
294 | Basically the child updates the state twice. I'm working on this.
|
295 |
|
296 | #### What does work?
|
297 |
|
298 | Our usage indicates that as long as we only move items between lists that don't use the same `setState` function.
|
299 |
|
300 | I hope to provide an example soon.
|
301 |
|
302 | #### Solutions
|
303 |
|
304 | We don't have anything that works 100%, but here I'd like to spit ball some potential avenues to look down.
|
305 |
|
306 | - Use `onMove` to handle state changes instead of `onAdd`,`onRemove`, etc.
|
307 | - Create a Sortable plugin specifically for react-sortbalejs
|