react-sortablejs icon indicating copy to clipboard operation
react-sortablejs copied to clipboard

Where can I read the source code of the examples? Also when can nesting be supporte

Open Rihel opened this issue 3 years ago • 15 comments

With the mobx library, the list data is all responsive, so nesting will be much easier, making it similar to Vuedraggable?

Rihel avatar Jan 23 '22 16:01 Rihel

https://codesandbox.io/s/react-sortablejs-examples-pejkn2

madeinspace avatar Mar 23 '22 09:03 madeinspace

I didn't fully understand the issue with nesting. I've been using nesting for the feature I'm building and it works just fine, I can drag from anywhere to anywhere.

julienben avatar Apr 28 '22 22:04 julienben

@julienben, could you please put out a code sandbox example for nested DND with a parent-child relationship between items?

ayushm-agrawal avatar May 31 '22 19:05 ayushm-agrawal

https://codesandbox.io/s/react-sortablejs-nested-demo-nouiv3

Very simplified version of my tool. You have groups and items. You can nest infinitely in any way you want (except items, which do not accept children, but you can make it all groups if you need).

julienben avatar Jun 01 '22 15:06 julienben

can i close this issue tough?

andresin87 avatar Jun 08 '22 06:06 andresin87

@andresin87 I'm guessing you can close but I was wondering if you had any thoughts on my nesting example above? There's only one "setState" function, it uses DFS to know which nesting to update.

julienben avatar Jun 08 '22 12:06 julienben

Nesting does work fine, but all lists must basically use the same setState as @julienben pointed out.

Another way that I've recently implemented ( found with a bunch of random google searching ), was passing both a top level setList method as well as an indexs array down to each child list, which contains that lists index in the parent list.

This allows easily manipulating the list state by using indexs for traversal.

The child list setList looks something like this:

        setList={currentList => {
          setList(rootList=> {
            const newRootList= [...rootList];
            const _indexs = [...indexs];
            const currentListIndex = _indexs.pop();
            
            // Get ref to currentLst inside rootList.
            const currentListRef= _indexs.reduce(
              (arr, i) => arr[i]["children"],
              tempList
            );
            
            // Replace with new updated list.
            currentListRef[currentListIndex ]["children"] = currentList;
            
            return newRootList;
          });
        }}

lastArr in this case is a reference to the curennt list inside the nested structure, setting its value there updates it inside the root list.

No extra packages needed, just a clever indexs based list traversal using references.

Full examples here:

  • https://codesandbox.io/s/misty-wood-g8lkj
  • https://codesandbox.io/s/react-sortable-js-nested-forked-ykxgpq

danieliser avatar Jun 12 '22 18:06 danieliser

One issue I'm facing is that it stops working with React 18. Not urgent since most other deps still need to catch up to it but it'd be a shame to be stuck to React 17 because of it.

julienben avatar Jun 15 '22 12:06 julienben

@danieliser Trying this approach, but both the parent and the child fire setList (first parent with original list, then child with updated, the parent with its updated but not including the child's updates), so I end up missing e.g. a child moved into a folder.

The parent shouldn't really need to update, but not sure how to best stop it.

Heilemann avatar Dec 29 '22 17:12 Heilemann

@julienben - We recently were forced to React 18 (product based on WordPress which upgraded to 18 recently), and I was pleasantly surprised our code still built & ran fine. None of the sortable/draggable stuff broke, and the nesting works fine still.

danieliser avatar Jan 06 '23 21:01 danieliser

@Heilemann - Happy to try and work it out, it was a rather confusing set of code which I heavily documented to not forget what exactly is occurring.

Do you have a sandbox?

danieliser avatar Jan 06 '23 21:01 danieliser

@danieliser Thank you, but I moved on to @dnd-kit. It's probably overall better suited for my needs anyway. I spent two weeks ramming my head against nested support with this library, and just couldn't ever get it to not be wonky ¯_(ツ)_/¯

Heilemann avatar Jan 06 '23 21:01 Heilemann

@Heilemann no worries. The biggest headache I had was when I tried to be clever and prevent redrawing entire list on every update. If you don't call the parent setlist which triggers a full redraw, then no changes get saved.

That said we were looking at dnd-kit as well, but previously we weren't looking to move to react-18 until our deps themselves moved up.

I also ended up rewriting it all using Context and hooks so that each parent creates a new context that children live in, but that really just means no passing props, the nesting setList concept still applied as each childContext calls parentContext inside it.

danieliser avatar Jan 06 '23 21:01 danieliser

Some updates on our implementation, which I just simplified greatly using contexts while also being more reliable.

I'm posting this here as a full working solution, albeit not the most out of the box simple solution, but it is working great, even on React 18. This should dispell the notes in the readme that it isn't possible. .Happy to boil this into a codesandbox without our extra flavor if its really needed.

This results in infinite nesting while minimizing added setState calls to the bare minimum with no recursion penalty.

A deeply nested item can modify itself without affecting its parents. which is extremely important to performance and reliability.

  1. Our setup uses a <List items={[]} onChange={...} indexs={ [0, 1] } /> component, as well as a ListContext which includes methods like addItem, removeItem etc.
  2. ListContext includes both a setList and setRootList methods as well as indexs array, isDragging and setIsDragging props handled by useControlledState etc.
  3. ListContext is then nestable, and when its nested it checks for a parent context, and if found passes the parent shared methods (ie setRootList etc) into the child context, if not it generates them as the root list.
  4. useControlledState is used to allow shared dragging status through the entire chain. We use it to add animated css borders to dropzones.
  5. Lastly is the nested setList method which I'll outline on its own below as it is warranted to prevent confusion due to its "magic" methodology which is totally solid JS magic.

The setRootList method and a few other consts you might need for reference.

Of note the setRootList method accepts both new value for entire list, or a callback that takes the existing (unchanged) list as a first argument for modification. Nested setList calls will use the latter, only root will pass value directly.

// Used for parent traversal when nested.
const parentQueryContext = useQuery(); // our custom context.

const isRoot = typeof parentQueryContext.isRoot === 'undefined';
const setRootList = isRoot
	? ( newList ) => {
			/**
			 * The root list can accept a functional state action.
			 * Check for that and call it with current items if needed.
			 */
			const _newList =
				typeof newList !== 'function'
					? newList
					: // Root list passes in current state to updaters.
						newList( items );

			// ! Warning, this is a bad idea. It results in changes not saving.
			// ! if ( isEqual( items, _newList ) ) {
			// * Prevents saving state during dragging as a suitable replacement.
			if ( isDragging ) {
				return;
			}

			onChange( {
				...query,
				items: _newList,
			} );
		}
	: parentQueryContext.setRootList;

And the context value containing the nestable setList method.

Biggest things to note here:

  1. If its not the root list, we pass in a callback to setRootList.
  2. That callback clones the entire tree.
  3. Then using the magic of .reduce on the indexs array and a clone of the full list as the starting variable, we are able to recursively walk our way down to the nested list we want to modify, returning a reference to that nested list within the full list.
  4. The reference from step 3 above is the entire magic. Modifying that reference by replacing it with the newList essentially modifies it in place in the cloned full root list.
  5. We then simply return the new modified copy of the root list within the setRootList callback.

So when moving items from one nested list to another, it makes 2 state save calls in general, one for the removal, one for the addition. No calling setState all the way back up the chain. A deeply nested item can modify itself without affecting its parents.

/**
 * Generate a context to be provided to all children consumers of this query.
 */
const queryContext: QueryContextProps = {
	...parentQueryContext,
	isRoot,
	indexs,
	isDragging,
	setIsDragging,
        items,
	setRootList,
	/**
	 * The root setList method calls the onChange method directly.
	 * Nested lists will then call setRootList and pass a SetStateFunctional
	 * that modifies the rootList based on the current list indexs list.
	 */
	setList: ( newList ) => {
		// Don't save state when dragging, will save a lot of calls.
		if ( isDragging ) {
			return;
		}

		// If its the root, check they aren't already equal( optional ), then pass the new list directly to the setRootList method.
		if ( isRoot ) {
			if ( ! isEqual( items, newList ) ) {
				setRootList( newList );
			}
		} else {
			setRootList( ( rootList ) => {
				// Clone root list.
				const newRootList = [ ...rootList ];

				// Clone indexs to current list.
				const parentIndexs = [ ...indexs ];

				// Remove this lists index from cloned indexs.
				const currentListIndex = parentIndexs.pop() ?? 0;

				/**
				 * Get reference to latest array in nested structure.
				 *
				 * This clever function loops over each parent indexs,
				 * starting at the root, returning a reference to the
				 * last & current nested list within the data structure.
				 *
				 * The accumulator start value is the entire root tree.
				 *
				 * Effectively drilling down from root -> current
				 *
				 * Return reference to child list for each parent index.
				 */
				const directParentList = parentIndexs.reduce(
					( arr, i ) => {
						const nextParentGroup = arr[ i ] as GroupItem;
						return nextParentGroup.query.items;
					},
					newRootList
				);

				// Get reference to current list from the parent list reference.
				const closestParentGroup = directParentList[
					currentListIndex
				] as GroupItem;

				// Prevent saving state if items are equal.
				if (
					! isEqual( closestParentGroup.query.items, newList )
				) {
					// Replaced referenced items list with updated list.
					closestParentGroup.query.items = newList;
				}

				return newRootList;
			} );
		}
	},

The full code is available here, fully typed in TypeScript:

Notes about our usage:

  • Groups (sublists) maintain their children in the .query.items property.

image

And one with the isDragging state, and you will notice only parents of the current item are sutable dropzones. A group can't drop into itself and our solution's CSS handles that nicely.

image

danieliser avatar Jan 16 '23 04:01 danieliser