GridStack + Vue 3 composition api
- среда, 27 декабря 2023 г. в 00:00:09
 
Мир всем, на связи ShADAMoV!
Сегодня я бы хотел поведать вам о своём опыте взаимодействия с библиотекой GridStack. Расскажу о странностях и сложностях, с которыми столкнулся в ходе её интеграции во Vue 3 проект.
Прежде чем приступим, дисклеймер: автор данной статьи не претендует на истину в последней инстанции и так же не рассказывает про саму технологию, а лишь делится опытом взаимодействия с ней. Прежде чем читать дальше, настоятельно рекомендую прочесть документацию по данной библиотеке от автора (это займет не больше 10 минут).
Итак, погнали!
Для начала, надо данную библиотеку - установить. Я использовал для этого npm
npm install --save gridstackСледующий шаг - подключение библиотеки в компонент.
import { GridStack } from 'gridstack';
import 'gridstack/dist/gridstack.min.css';Далее, нам надо определиться со способом, которым мы будем загружать данные для отображения на сетке. Их всего два: js или html.
Допустим у вас есть некий массив blocks, в котором лежат объекты с полями, необходимыми для установки размера и позиции элемента.
const blocks = [
  {
    w: 2,
    h: 2,
    x: 0,
    y: 0,
    id: 'Unic1',
    type: 'Standart',
  },
  {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      id: 'Unic2',
      type: 'Image',
    },
  {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      id: 'Unic3',
      type: 'Text',
    },
]Поле type нам может понадобиться в том случае, если добавляемые блоки отличны друг от друга (например: разные компоненты).
Первый способ. Отобразим данный массив при помощи js.
<template>
  <div class="grid-stack"></div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { GridStack } from 'gridstack';
import 'gridstack/dist/gridstack.min.css';
const blocks = [
  {
    w: 2,
    h: 2,
    x: 0,
    y: 0,
    id: 'Unic1',
    type: 'Standart',
  },
  {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      id: 'Unic2',
      type: 'Image',
    },
  {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      id: 'Unic3',
      type: 'Text',
    },
]
// Тут будут лежать минимальные настройки для GridStack
const gridStackOptions = {
  	animate: true,
	cellHeight: '180px',
	float: true,
	column: 12,
	margin: 4,
	resizable: {
		handles: 'e, se, s, sw, w, nw, n, ne',
	},
}
// Так как данная библиотека рабоатет с уже имеющимся HTML, выполняем всё в onMounted
onMounted(() => {
  const grid = GridStack.init(gridStackOptions, 'grid-stack')
  // Загружаем данные в сетку
  blocks.map((block) => {
    grid.addWidget({
      x: block.x,
      y: block.y,
      w: block.w,
      h: block.h,
      id: block.id,
    })
  }) 
})
</script>
<style lang=scss>
.grid-stack-item {
	z-index: 1;
	border: 2px solid gray;
	user-select: none;
	border-radius: 12px;
	&:hover {
		box-shadow: 0 4px 14px 0 rgb(0 0 0 / 10%);
	}
}
</style>
На данном этапе, сетка уже должна иметь 3 элемента, которые вы можете свободно перемещать и изменять их размер. Минус такого подхода в том, что при добавлении не просто блока, а некоего компонента, начинаются танцы с бубнами, где рискуешь потерять реактивность, так как в этом случае, можно передавать содержимое widget'а только в виде html.
Второй способ (которым я и воспользовался), больше подходит под большинство задач, так как автор данной библиотеки, пользуется фрэймворком Angular, из‑за чего в фундамент заложено декларативное встраивание контента.
<template>
  <div class="grid-stack">
    <div 
      v-for="block in blocks" 
      :key="block.id"
      :gs-x="block.x"
      :gs-y="block.y"
      :gs-w="block.w"
      :gs-h="block.h"
      :gs-id="block.id" 
      class="grid-stack-item"
    >
        <div class="grid-stack-item-content">
            <standart v-if="block.type === 'standart'" />
            <text v-if="block.type === 'text'" />
        </div>
      </div>  
  </div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { GridStack } from 'gridstack';
import 'gridstack/dist/gridstack.min.css';
import Standart from '../standart/index.vue';
import LineChart from '../text/index.vue';
const blocks = [
  {
    w: 2,
    h: 2,
    x: 0,
    y: 0,
    id: 'Unic1',
    type: 'standart',
  },
  {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      id: 'Unic2',
      type: 'image',
    },
  {
      w: 2,
      h: 2,
      x: 0,
      y: 0,
      id: 'Unic3',
      type: 'text',
    },
]
// Тут будут лежать минимальные настройки для GridStack
const gridStackOptions = {
  	animate: true,
	cellHeight: '180px',
	float: true,
	column: 12,
	margin: 4,
	resizable: {
		handles: 'e, se, s, sw, w, nw, n, ne',
	},
}
// Так как данная библиотека рабоатет с уже имеющимся HTML, выполняем всё в onMounted
onMounted(() => {
  GridStack.init(gridStackOptions, 'grid-stack')
})
</script>
<style lang=scss>
.grid-stack-item-content {
	z-index: 1;
	border: 2px solid gray;
	user-select: none;
	border-radius: 12px;
	&:hover {
		box-shadow: 0 4px 14px 0 rgb(0 0 0 / 10%);
	}
}
</style>По сути, вы сделали всё, что нужно было для того, чтобы начать пользоваться GridStack. "Мои поздравления!" хотел бы я сказать, однако далее я расскажу о нервотрёпке, с которой я столкнулся...
По идее, GridStack - это сетка, на которой можно размещать, перемещать и изменять блоки, которые мы на ней отобразим, однако какого было моё удивление, когда я узнал, что отобразить сетку background'ом - НЕЛЬЗЯ!
Сказать, что я был расстроен - ничего не сказать... Мне пришлось искать способы отображения сетки человеческим способом. Я пробовал и background-image, и liner-gradient, и через absolute размещенный позади сетки html-элемент. Скажу сразу, остановился на последнем способе, так как у меня была не просто сетка, а сетка, элементы которой прямоугольники, с закругленными углами...
Если ваша цель - просто сетка, без закруглений и прочей вундердизайнерской чепухи - ваш выбор linear-gradient.
<style lang="scss">
.grid-stack {
	background: #f7f7f7;
	background-image: linear-gradient(#fff 8px, transparent 8px),
		linear-gradient(90deg, #fff 8px, transparent 8px);
	background-size: 8.33% 180px, 8.33%;
	background-position: 0 -4px, -4px 0;
}
.grid-stack-item-content {
	z-index: 1;
	border: 2px solid gray;
	user-select: none;
	border-radius: 12px;
	background-color: white;
	&:hover {
		box-shadow: 0 4px 14px 0 rgb(0 0 0 / 10%);
	}
}
</style>
Но если же вы из числа мазохистов, то я лишу вас удовольствия получить всю ту боль, что я пережил и покажу, как можно отрисовать красивые прямоугольники с закруглёнными углами.
<template>
	<div class="grid-stack">
		<div
			v-for="block in blocks"
			:key="block.id"
			:gs-x="block.x"
			:gs-y="block.y"
			:gs-w="block.w"
			:gs-h="block.h"
			:gs-id="block.id"
			class="grid-stack-item"
		>
			<div class="grid-stack-item-content">
				<standart v-if="block.type === 'standart'" />
				<text v-if="block.type === 'text'" />
			</div>
		</div>
		<div class="background">
			<!-- 4 - количество строк сетки, если сетка пуста -->
			<div v-for="n in blocks.length ? gridRowCount : 4" :key="n" class="background__row">
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
				<div class="background__row-item"></div>
			</div>
		</div>
	</div>
</template>
<script setup lang="ts">
import { onMounted, computed, ref } from 'vue';
import { GridStack } from 'gridstack';
import 'gridstack/dist/gridstack.min.css';
const gridRowCount = ref<number>();
const blocks = [
	{
		w: 2,
		h: 2,
		x: 0,
		y: 0,
		id: 'Unic1',
		type: 'standart',
	},
	{
		w: 2,
		h: 2,
		x: 0,
		y: 0,
		id: 'Unic2',
		type: 'image',
	},
	{
		w: 2,
		h: 2,
		x: 0,
		y: 0,
		id: 'Unic3',
		type: 'text',
	},
];
// Тут будут лежать минимальные настройки для GridStack
const gridStackOptions = {
	animate: true,
	cellHeight: '188px',
	float: true,
	column: 12,
	margin: 4,
	resizable: {
		handles: 'e, se, s, sw, w, nw, n, ne',
	},
};
// Так как данная библиотека рабоатет с уже имеющимся HTML, выполняем всё в onMounted
onMounted(() => {
	const grid = GridStack.init(gridStackOptions, 'grid-stack');
	// Так как grid инницилизируется какое-то время, сразу получить данные о количестве строк - не удастся, поэтому ждём какое-то время
	setTimeout(() => {
		gridRowCount.value = grid.getRow();
		console.log(gridRowCount);
	}, 200);
});
</script>
<style lang="scss">
.grid-stack-item {
	position: relative;
}
.grid-stack-item-content {
	z-index: 1;
	border: 2px solid #e5e7ee;
	user-select: none;
	border-radius: 12px;
	background-color: white;
	&:hover {
		box-shadow: 0 4px 14px 0 rgb(0 0 0 / 10%);
	}
}
.background {
	position: absolute;
	width: 100%;
	height: 100%;
	&__row {
		display: flex;
		width: 100%;
		height: 188px;
		column-gap: 10px;
		padding: 4px;
		&-item {
			width: 8.3333%;
			width: calc((100% - 110px) / 12);
			height: 100%;
			border: 1px solid #e5e7ee;
			border-radius: 13px;
		}
	}
}
</style>
Я бы наверное еще написал про проблемы, возникающие при адаптации и работе данной библиотеке в связке с данными с бэка, однако написание статьи и так уже затанулось на 2 часа, а спать как бы хочется)
Надеюсь, что данная статья была для вас полезна! Всегда рад комментариям и обоснованной критике.
Всем мира <( ̄︶ ̄)>