【ag-Grid】グリッド間のドラッグ&ドロップ

はじめに

こんにちは、kanoです。
先日、JavaScriptのグリッドライブラリであるag-Gridを検証しました。
ag-Gridの機能は多岐に渡りますが、今回はグリッド間でデータをドラッグ&ドロップする機能の実装方法をご紹介します。

ag-Grid: Dragging Multiple Records Between Grids
https://www.ag-grid.com/javascript-grid-row-dragging-to-grid/#dragging-multiple-records-between-grids

完成イメージ

左側は選択候補のグリッド、右側は選択済データのグリッドです。

以下の操作を想定しています。

①任意のデータを選択

②左のグリッドから右のグリッドにドラッグ&ドロップ

▲赤枠の部分をマウスでドラッグします

▲ドロップすると、左側のデータがなくなり右側に追加されます

③右から左にドラッグ&ドロップして選択を取り消し

▲同様に右から左への移動も可能です

検証バージョン

  • Vue.js:v2.6.11
  • ag-Grid:v23.1.1
     ※有償版(ag-Grid Enterprise)

実装内容

ソースコードは以下です。
スクロールしてソースコード全体が確認できます。(ツールバーから新しいウィンドウでコードを開くことも可能です)

<template>
  <div>
    <div style=" float: left;">
      <h6>選択一覧:</h6>
      <AgGridVue
        style="width: 500px; height: 300px;"
        class="ag-theme-balham"
        :grid-options="leftGridOptions"
        :row-data="leftRowData"
        :modules="modules"
        @grid-ready="onLeftGridReady"
      />
    </div>
    <div style="float: left; margin-left: 10px;">
      <h6>選択済み:</h6>
      <AgGridVue
        style="width: 500px; height: 300px;"
        class="ag-theme-balham"
        :grid-options="rightGridOptions"
        :row-data="rightRowData"
        :modules="modules"
        @grid-ready="onRightGridReady"
      />
    </div>
  </div>
</template>

<script>
import { AgGridVue } from '@ag-grid-community/vue'
import { AllModules } from '@ag-grid-enterprise/all-modules'
import { SampleColumns, SampleData } from './SampleData.js'

export default {
  name: `Sample`,
  components: {
    AgGridVue
  },
  data() {
    return {
      leftGridOptions: null,
      rightGridOptions: null,
      leftRowData: SampleData(),
      rightRowData: [],
      modules: AllModules
    }
  },
  beforeMount() {
    // グリッドのオプションを設定(今回は左右全く同じ)
    this.leftGridOptions = {
      defaultColDef: {
        flex: 1,
        minWidth: 100,
        sortable: true,
        resizable: true,
        filter: true,
        floatingFilter: true
      },
      suppressKeyboardEvent: () => true,
      rowSelection: 'multiple',
      rowMultiSelectWithClick: true,
      enableMultiRowDragging: true,
      rowDragManaged: true,
      getRowNodeId: (data) => data.publisherId + '-' + data.siteId, // rowNodeIdがユニークになるように設定
      columnDefs: SampleColumns(),
      animateRows: true
    }
    this.rightGridOptions = {
      defaultColDef: {
        flex: 1,
        minWidth: 100,
        sortable: true,
        resizable: true,
        filter: true,
        floatingFilter: true
      },
      suppressKeyboardEvent: () => true,
      rowSelection: 'multiple',
      rowMultiSelectWithClick: true,
      enableMultiRowDragging: true,
      rowDragManaged: true,
      getRowNodeId: (data) => data.publisherId + '-' + data.siteId,
      columnDefs: SampleColumns(),
      animateRows: true
    }
  },
  methods: {
    // 左のグリッドのgridReadyイベント処理
    onLeftGridReady(params) {
      // ドロップゾーンの設定
      this.addGridDropZone(params, this.rightGridOptions.api)
    },
    // 右のグリッドのgridReadyイベント処理
    onRightGridReady(params) {
      // ドロップゾーンの設定
      this.addGridDropZone(params, this.leftGridOptions.api)
    },
    // ドロップゾーン設定処理
    addGridDropZone(params, dropZoneApi) {
      const thisGridApi = params.api
      const dropZoneParams = dropZoneApi.getRowDropZoneParams({
        // ドロップ終了後にドロップ元の該当行を削除
        onDragStop(params) {
          thisGridApi.applyTransaction({
            remove: params.nodes.map(function(node) {
              return node.data
            })
          })
        }
      })
      thisGridApi.addRowDropZone(dropZoneParams)
    }
  }
}
</script>
export const SampleColumns = () => {
  return [
    {
      rowDrag: true,
      rowDragText(params, dragItemCount) {
        // ドラッグ時に表示するテキストを設定
        return dragItemCount + ' rows'
      },
      maxWidth: 50,
      suppressMenu: true,
      filter: false
    },
    {
      checkboxSelection: true,
      headerCheckboxSelection: true,
      maxWidth: 50,
      suppressMenu: true,
      filter: false
    },
    {
      headerName: '媒体社ID',
      field: 'publisherId',
      hide: true
    },
    {
      headerName: '媒体社名',
      field: 'publisherName',
      filter: 'agTextColumnFilter'
    },
    {
      headerName: 'サイトID',
      field: 'siteId',
      hide: true
    },
    {
      headerName: 'サイト名',
      field: 'siteName',
      filter: 'agTextColumnFilter'
    }
  ]
}

export const SampleData = () => {
  const data = []
  // テストデータ:媒体社1~5に10サイトずつ紐付け
  for (let i = 1; i <= 5; i++) {
    for (let j = 1; j <= 10; j++) {
      data.push({
        publisherId: 'pubId' + i,
        publisherName: '株式会社' + i,
        siteId: 'siteId' + String((i - 1) * 10 + j),
        siteName: 'サイト' + String((i - 1) * 10 + j)
      })
    }
  }
  return data
}

〇実装のポイント

  • GridOptions
    • rowDragManaged:ドラッグの有効化
    • getRowNodeId:左右のグリッドでrowNodeIdが同じになるよう、一意の値を設定
      rowDragManaged: true,
      getRowNodeId: (data) => data.publisherId + '-' + data.siteId, // rowNodeIdがユニークになるように設定
  • ドロップゾーンの設定
    • 左右ぞれぞれのgridApiを使用し、相互にドロップゾーンを設定
    • ドラッグ&ドロップした際に、移動元のデータを削除する設定を追加
    // ドロップゾーン設定処理
    addGridDropZone(params, dropZoneApi) {
      const thisGridApi = params.api
      const dropZoneParams = dropZoneApi.getRowDropZoneParams({
        // ドロップ終了後にドロップ元の該当行を削除
        onDragStop(params) {
          thisGridApi.applyTransaction({
            remove: params.nodes.map(function(node) {
              return node.data
            })
          })
        }
      })
      thisGridApi.addRowDropZone(dropZoneParams)
    }
  • ドラッグ用のカラムを表示
    • カラム定義にて、1つ目をドラッグ用のカラムに設定
      ※rowDragTextは選択行数を表すように設定しています。選択行のデータを表示したりとカスタマイズが可能です。
    {
      rowDrag: true,
      rowDragText(params, dragItemCount) {
        // ドラッグ時に表示するテキストを設定
        return dragItemCount + ' rows'
      },
      maxWidth: 50,
      suppressMenu: true,
      filter: false
    },

注意点

通常、 gridOptions.pagination = true を設定することでページングが可能ですが、本機能との併用は不可のようでした。

▲一般的なグリッドはページング機能(赤枠部分)を付けることができます

おわりに

ag-Gridではグリッド間のドラッグ&ドロップが簡単に実装できることが分かりました。
この検証に伴い、コンテキストメニュー(右クリックメニュー)とショートカットキーのカスタマイズも行ったので、次回はその方法をご紹介したいと思います。