Slate.js + dnd-kit = 🔥. Improving Rich Text Editor UX by adding Drag and Drop.
Tags: javascript, react, slatejs, dndkit
Published: 2022-06-04 18:24:34

Sometimes writing experience may be improved by having the possibility to reorder content blocks. Tools like _notion_ and note taking apps have set this trend. I, personally, like this feature especially when I work with lists.

![Drag and Drop preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6mcrlr9b7mhbuax2yk4.gif)

In this post I want to share an idea on how to inject `dnd-kit` toolkit into Rich Text Editor based on `slate.js`.

> _Code that you will see in examples here doesn't work as it is. It just illustrates the main ideas. However, working examples in codesandbox are provided._

I tried to keep only necessary information without getting deep into details.

Let's start!

## slate.js
Slate.js is a great framework for building your own rich text editors. You can read about use cases on their documentation page: https://docs.slatejs.org/.

For the really first and simple version of editor we need the following components: `Slate`,  `Editable` and `DefaultElement`.
- `Slate` component is more like a react context that provides value, editor instance and some other useful things.
- `Editable` component renders all nodes and provides right properties to them.
- `DefaultElement` is a simple `div` or `span` element with applied properties that `renderElement` receives (`attributes`, `children`, `element`).

The following code should explain how it works in just a few lines of code:

```jsx
const App = () => 
  <Slate value={value}>
    <Editable renderElement={renderElement} />
  </Slate>

const renderElement = ({ attributes, children, element }) =>
  <DefaultElement attributes={attributes} children={children} element={element} />
```

_sandbox: https://codesandbox.io/s/slate-kv6g4u_

> _deps: `yarn add slate slate-react`_

I want to share some details about how slate works that are important for current topic:
- Slate value has `Descendant[]` type.
- `type Descendant = Element | Text`.
- `Element`
```typescript
interface Element {
  children: Descendant[];  
}
```
- `Text`
```typescript
interface Text {
  text: string;  
}
```
- Consequently slate value is a tree.
- All nodes that present in value are rendered by the `Editable` component. We can specify the `renderElement` function to define each element's appearance.

Good start, let's continue with exploring `dnd-kit`.



## dnd-kit

This toolkit is really useful for building Drag and Drop interfaces. It provides nice primitives to build your own dnd logic maximum customizable way. You can find all information here: https://dndkit.com/

Few words about how it is supposed to be applied in the app. It provides the following API: 
- `DndContext`
- `useDraggable`
- `useDroppable`

We can wrap dnd area into `DndContext`, then inside this area apply `useDraggable` hook to draggable elements and `useDroppable` hook to droppable elements.

But we won't use it this way for sorting because dnd-kit already provides higher level API for it:
- `SortableContext`
- `useSortable`

One more component we need is:
- `DragOverlay`. This component will be rendered on document `body` level and next to the mouse cursor temporarily while dragging.

Let's show how we can use it. This example is intended to demonstrate how `dnd-kit` works itself, _without_ slate.js. You can see how components related to each other:

```jsx
const App = () => 
  <DndContext>
    <SortableContext>
      {items.map(item => <SortableItem item={item} />)}
      {createPortal(
        <DragOverlay>
          {activeItem && renderItemContent({ item: activeItem })}
        </DragOverlay>,  
        document.body  
      )}
    </SortableContext>
  </DndContext>

const SortableItem = ({ item }) => {
  const sortable = useSortable({ id: item.id });

  return <Sortable sortable={sortable}>
    <button {...sortable.listeners}>⠿</button>
    {renderItemContent({ item })}
  </Sortable>
}

const renderItemContent = ({ item }) => <div>{item.value}</div>
```
_sandbox: https://codesandbox.io/s/dnd-kit-4rs8rz_

> _deps: `yarn add @dnd-kit/core @dnd-kit/sortable`_

You might notice, there is a `Sortable` component I didn't mention before. It is a simple component that applies `sortable` props to `div`. The props like `transition` and `transform`. You can find its implementation in the sandbox.

There is also a `button` component that we use like a dnd handle by applying `listeners` to it.





## slate.js + dnd-kit

I hope after the previous parts you become a bit more familiar with these libraries in case you haven't used them before. It's time to combine them.

Generally we need to do the following steps:
- Wrap `Editable` into `DndContext` and `SortableContext`
- Adjust `renderElement` function _only_ for top level elements. We will render `SortableElement` component with `useSortable` hook inside.
- For `DndOverlay` create `DndOverlayContent` component with temporary slate editor, that renders just one dragging element.

The code is here:

```jsx
const App = () => {
  const renderElement = useCallback((props) => {
    return isTopLevel
           ? <SortableElement {...props} renderElement={renderElementContent} />
           : renderElementContent(props);
  }, []);

  return <Slate value={value}>
    <DndContext>
      <SortableContext>
        <Editable renderElement={renderElement} />
        {createPortal(
          <DragOverlay>
            {activeElement && <DragOverlayContent element={activeElement} />}
          </DragOverlay>,  
          document.body  
        )}
      </SortableContext>
    </DndContext>
  </Slate>
}

const SortableElement = ({
  attributes,
  element,
  children,
  renderElement
}) => {
  const sortable = useSortable({ id: element.id });

  return (
    <div {...attributes}>
      <Sortable sortable={sortable}>
        <button contentEditable={false} {...sortable.listeners}>
          ⠿
        </button>
        <div>{renderElement({ element, children })}</div>
      </Sortable>
    </div>
  );
};

const renderElementContent = (props) => <DefaultElement {...props} />;

const DragOverlayContent = ({ element }) => {
  const editor = useEditor();
  const [value] = useState([JSON.parse(JSON.stringify(element))]); // clone

  return (
    <Slate editor={editor} value={value}>
      <Editable readOnly={true} renderElement={renderElementContent} />
    </Slate>
  );
};
```

_sandbox: https://codesandbox.io/s/slate-dnd-kit-brld4z_

> _deps: `yarn add nanoid slate slate-react @dnd-kit/core @dnd-kit/sortable`_

_styled example: https://codesandbox.io/s/slate-dnd-kit-styled-7qjxm3_

### Assigning ids to new nodes
This is necessary to have unique ids for each sorting element. We pass an array of ids into `SortableContext` with `items` prop. And we also pass an id for each element to `useSortable` hook.
Creating new elements is a process that slate does by itself. For example, when the `Enter` key is pressed. However, we can add a plugin that assigns unique ids for new elements. You can find the `withNodeId` plugin in the sandbox above.

### Performance
I recommend checking if the performance of this solution is suitable for you. If the editor renders many elements it could be slow. If you want to prevent it, then you can try to use html5 dnd instead, for example, `react-dnd` library. Or you can try to adjust this solution to use partial virtualization technique. Just render the Sortable wrapper only for elements that are in viewport.

> Here you can find some words about HTML5 drag and drop https://docs.dndkit.com/#architecture

## Last part

As I said before this post is intended to share an idea. It might require way more coding to fix all possible issues and make it work perfectly in production. However, it might be brought to a well level user experience.

I hope you find it useful. If you have any questions feel free to ask. I would also like to receive your feedback. And if you implement similar functionality the same or different way, please share it. It is really interesting for me. Thanks!