【Vue3】gridstack.jsでドラッグ可能なダッシュボードを実装する

記事タイトルとURLをコピーする

アプリケーションサービス部の鎌田(義)です。

今回は、
ドラッグ/ドロップやリサイズ、追加/削除が可能な自由度の高いダッシュボードが実装できる gridstack.jsというライブラリを紹介したいと思います。

本記事ではVue3を使用し、基本的な使用方法について記載します。

gridstackとは

gridstackの特徴

公式ページにも記載されていますが、
純粋なTypeScriptであり、フレームワークを選ばない点が大きな特徴かと思います。
今回は、OSSかつVue3でも利用できるという点でgridstackを選択しました。

その他の類似サービス

類似したサービスとしては以下のようなものがあります。

環境構築

Vue3環境作成

$ mkdir demo_gridstack
$ cd demo_gridstack/
$ npm create vite@latest .
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

$ npm install
$ npm run dev

http://localhost:5173/ にアクセスして 画面が表示できることを確認します。

gridstackインストール

$ npm install --save gridstack@8.3.0

コードの実装

スタイル修正

Bootstrapを使用する為、最低限の修正を加えます。

index.html

linkを2行追記してタイトルもついでに修正しています。

src/main.ts

スタイルのインポートをコメントアウトしています。

デモ(移動/リサイズ)

src/App.vue

デモ用のコンポーネントを読み込むよう修正しています。
※適宜DemoGridStack*.vueを置き換えます。

src/components/DemoGridStackBasic.vue

今回は一部のみパラメータを指定していますが、 詳しくはドキュメントをご確認下さい。

gridOptionsでは今回、以下を指定しました。

const gridOptions: GridStackOptions = {
  float: true, // 縦方向にウィジェットを自動的に詰めないようtrueを指定。デフォルトはfalse
  cellHeight: 100, // ウィジェットの高さを100pxに指定。デフォルトは自動
  disableResize: false, // ウィジェットのリサイズを許可する為falseを指定。デフォルトはfalse
  maxRow: 20, // 最大行数を20に指定。ウィジェットの高さ(h)が2の場合、10個まで縦に並べることができる。デフォルトは最大値なし
};

initWidgetsでは、初期表示するウィジェットを4つ用意しています。

const initWidgets: GridStackWidget[] = [
  { id: "initWidget-1", w: 2, h: 2 },
  { id: "initWidget-2", w: 2, h: 2 },
  { id: "initWidget-3", w: 2, h: 2 },
  { id: "initWidget-4", w: 2, h: 2 },
];

サイズは2×2とし、
x軸やy軸は今回は使用せず、配列の順番通りに並ぶようになっています。

マウント時にgridstackインスタンスを生成しています。
nextTick()でDOM更新が完了した後にmakeWidgets()を呼び出し、
初期ウィジェットをgridstackインスタンスに追加しています。

onMounted(() => {
  grid = GridStack.init(gridOptions);
  nextTick(() => {
    makeWidgets();
  });
});
  
const makeWidget = (widget: GridStackWidget): void => {
  const elSelector = `#${widget.id}`;
  grid.makeWidget(elSelector);
};
  
const makeWidgets = (): void => {
  widgets.value.forEach((widget) => {
    makeWidget(widget);
  });
};

template部分では、
gridstackが管理する要素を特定する為、指定のclass名を使用します。

<template>
  <div class="grid-stack"> <!--gridstackの親要素-->
    <div  <!--gridstackの子要素-->
      v-for="widget in widgets"
      :id="widget.id"
      :key="widget.id"
      :gs-id="widget.id"
      :gs-x="widget.x"
      :gs-y="widget.y"
      :gs-h="widget.h"
      :gs-w="widget.w"
    >
      <div class="grid-stack-item-content card shadow">  <!--gridstackの子要素のコンテンツ-->
        <div class="col d-flex align-items-center justify-content-center">
          <span class="text">{{ widget.id }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

デモ(追加/削除/レイアウト保存)

src/components/DemoGridStack.vue

gridOptionsでは、handleClassを追加、disableResizeをtrueに変更しました。

const gridOptions: GridStackOptions = {
  float: true,
  cellHeight: 100,
  handleClass: "draggable",  // ドラッグ可能な要素のクラス名を指定。デフォルトはnull
  disableResize: true,
  maxRow: 20,
};

addNewWidget関数では、
ランダムなサイズのウィジェットを追加し、gridstackインスタンスへの追加を行っています。
簡易的にユニークなIDを生成しています。
また、こちらもDOM更新後にgridstackインスタンスへ追加するようにしています。

const addNewWidget = (): void => {
  const uniqueId = Date.now().toString(16);
  const widget = {
    id: `widget-${uniqueId}`,
    w: Math.floor(Math.random() * 2) ? 2 : 4,
    h: Math.floor(Math.random() * 2) ? 2 : 4,
  };
  widgets.value.push(widget);
  
  nextTick(() => {
    makeWidget(widget);
  });
};

gridstackの子要素にヘッダを用意し、gridOptionsで指定した「draggable」をクラス名に加えました。
ドラッグ可能な要素を分かりやすくする為、カーソルを変更しています。

<!--省略-->
      <div class="grid-stack-item-content card shadow">
        <div class="bg-light d-flex">
          <div class="flex-grow-1 draggable"></div>
          <div>
            <button class="btn" @click="deleteWidget(widget.id)">
              <i class="bi bi-x-lg"></i>
            </button>
          </div>
        </div>
        <div class="col d-flex align-items-center justify-content-center">
          <span class="text">{{ widget.id }}</span>
        </div>
      </div>
    </div>
  </div>
</template>
  
<style scoped>
.draggable {
  cursor: move;
}
</style>

整列ボタンでは、compactメソッドを使い空白を埋めて詰めるようにしています。

    <button class="m-1 btn btn-secondary" @click="grid.compact()">整列</button>

レイアウトの保存では、saveメソッドで出力されるウィジェットの配列を使用して
ローカルストレージに保存しています。

const saveLayout = (): void => {
  const layouts = grid.save();
  localStorage.setItem("gridstack-layout", JSON.stringify(layouts));
};

マウント時に、ローカルストレージにレイアウトが存在する場合はロードしています。
onメソッドでは第一引数にdragstopイベントを指定し、infoメッセージにドラッグ後の座標を入れています。

onMounted(() => {
  grid = GridStack.init(gridOptions);
  loadLayout();
  nextTick(() => {
    makeWidgets();
  });
  
  grid.on("dragstop", (_: DragStopParams[0], element: DragStopParams[1]) => {
    const node = element.gridstackNode;
    info.value = `you just dragged node #${node?.id} to {x: ${node?.x}, y: ${node?.y}}`;
  });
});

まとめ

いかがでしたでしょうか。
比較的少ないコードで自由度の高いダッシュボードが実装できました。
今回は、味気ないウィジェットのみでしたがChart.jsなどのチャートライブラリを使用して
グラフウィジェットを組み込むことも可能です。
ぜひ試してみてください。

鎌田 義章 (執筆記事一覧)

2023年4月入社 AS部DS2課