33
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Material-UI で並べ替え可能なリストを作る

Last updated at Posted at 2019-03-26

Material-UI では、Material Design Guidelines で定められているドラッグ&ドロップで並べ替えられるリストが提供されていません。ドラッグ&ドロップの機能を提供するライブラリと組み合わせることで、これを実現できます。

ドラッグ&ドロップ

React でドラッグ&ドロップを実現するライブラリは数多ありますが、今回は以下の4つを比較・検討しました。箇条書きはあくまで個人的な感想です。

react-dnd

react-dnd/react-dnd: Drag and Drop for React

  • 一番有名どころ。オリジナルの作者は、React や Redux の Dan Abramov (gaearon) 氏。
  • 拡張性が高いので、その気になればいろんなことができそう。一方で、アニメーションなど自力で実装しなければならない部分が多い。
  • 記述量は多め。

react-beautiful-dnd

atlassian/react-beautiful-dnd: Beautiful and accessible drag and drop for lists with React

  • Atlassian 製。
  • ソートのアニメーションが優れている。記述量は多め。
  • “Not for everyone” だそうだ。たとえば、今回必要な「軸の固定 (axis locking) 」は、何度か機能提案されているが、UX を損なう (意訳) としてすべて却下されている。
  • ということで、“Not for me” だった。

react-sortable-hoc

clauderic/react-sortable-hoc: A set of higher-order components to turn any list into an animated, touch-friendly, sortable list ✌️

  • 記述量が少なくて済むので、実装が楽。
  • 現在非推奨な findDOMNode が使われていてレガシーな感じ。
  • Material-UI で DragHandle をやろうとするとちょっとハマった (後述)。

react-smooth-dnd

kutlugsahin/react-smooth-dnd: react wrapper components for smooth-dnd

  • Star の数では劣るが、特に問題はなさそう。
  • 記述量は少なくて済む。ハマりポイントもなく、一番素直に実装できた。
  • タッチデバイスだと難ありらしい (Issue が上がっている)。

実装例

react-sortable-hoc で実装してみた例と、react-smooth-dnd で実装してみた例を記載します。(手元で実装したものと CodeSandbox に上げたもので、使用したパッケージのバージョンが異なりますが、動作に問題はありません。)

react-sortable-hoc

Edit Material-UI Sortable List with react-sortable-hoc

使用したパッケージ

  • React v16.8.5
  • Material-UI v3.9.2
  • react-sortable-hoc v1.8.3
  • array-move v2.0.0
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
import arrayMove from 'array-move';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import DragHandleIcon from '@material-ui/icons/DragHandle';

const DragHandle = SortableHandle(() => (
  <ListItemIcon>
    <DragHandleIcon />
  </ListItemIcon>
));

const SortableItem = SortableElement(({ text }) => (
  <ListItem ContainerComponent="div">
    <ListItemText primary={text} />
    <ListItemSecondaryAction>
      <DragHandle />
    </ListItemSecondaryAction>
  </ListItem>
));

const SortableListContainer = SortableContainer(({ items }) => (
  <List component="div">
    {items.map(({ id, text }, index) => (
      <SortableItem key={id} index={index} text={text} />
    ))}
  </List>
));

const SortableList = () => {
  const [items, setItems] = useState([
    { id: '1', text: 'Item 1' },
    { id: '2', text: 'Item 2' },
    { id: '3', text: 'Item 3' },
    { id: '4', text: 'Item 4' }
  ]);

  const onSortEnd = ({ oldIndex, newIndex }) => {
    setItems(items => arrayMove(items, oldIndex, newIndex));
  };

  return (
    <SortableListContainer
      items={items}
      onSortEnd={onSortEnd}
      useDragHandle={true}
      lockAxis="y"
    />
  );
};

ReactDOM.render(<SortableList />, document.getElementById('root'));

ハマった点ですが、ドラッグ中の li 要素が document.body にクローンされるため、リストのマーカーが表示されてしまって、表示崩れを起こしてしまいました。これを回避するにはいくつかの方法があります。

  • bodylist-style-type: none; を適用する。
  • ListItemSecondaryActionSortableHandle に含めてしまう。
  • ListItemContainerComponent="div" を指定する。

実装例では3番目の方法を採用しています。ListItem だけ変更すると、実際の DOM で ul の直下に div がきて気持ち悪いので、List にも component="div" を指定しています。

react-smooth-dnd

Edit Material-UI Sortable List with react-smooth-dnd

使用したパッケージ

  • React v16.8.5
  • Material-UI v3.9.2
  • react-smooth-dnd v0.8.2
  • array-move v2.0.0
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Container, Draggable } from 'react-smooth-dnd';
import arrayMove from 'array-move';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import DragHandleIcon from '@material-ui/icons/DragHandle';

const SortableList = () => {
  const [items, setItems] = useState([
    { id: '1', text: 'Item 1' },
    { id: '2', text: 'Item 2' },
    { id: '3', text: 'Item 3' },
    { id: '4', text: 'Item 4' }
  ]);

  const onDrop = ({ removedIndex, addedIndex }) => {
    setItems(items => arrayMove(items, removedIndex, addedIndex))
  };

  return (
    <List>
      <Container dragHandleSelector=".drag-handle" lockAxis="y" onDrop={onDrop}>
        {items.map(({ id, text }) => (
          <Draggable key={id}>
            <ListItem>
              <ListItemText primary={text} />
              <ListItemSecondaryAction>
                <ListItemIcon className="drag-handle">
                  <DragHandleIcon />
                </ListItemIcon>
              </ListItemSecondaryAction>
            </ListItem>
          </Draggable>
        ))}
      </Container>
    </List>
  );
};

ReactDOM.render(<SortableList />, document.getElementById('root'));

先述の通り、現バージョンではタッチデバイスで難ありらしいですが、PC 向けにはこれで問題ないように思います。

余談

いずれ React Hooks を使って useDragAndDrop みたいに書けるようになるかもしれないですね。実装が楽になって、見通しがよくなるといいですね。

33
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?