Мир всем, на связи ShADAMoV!

Сегодня я бы хотел поведать вам о своём опыте взаимодействия с библиотекой GridStack. Расскажу о странностях и сложностях, с которыми столкнулся в ходе её интеграции во Vue 3 проект.

Прежде чем приступим, дисклеймер: автор данной статьи не претендует на истину в последней инстанции и так же не рассказывает про саму технологию, а лишь делится опытом взаимодействия с ней. Прежде чем читать дальше, настоятельно рекомендую прочесть документацию по данной библиотеке от автора (это займет не больше 10 минут).

Итак, погнали!

  1. Для начала, надо данную библиотеку - установить. Я использовал для этого npm

    npm install --save gridstack
  2. Следующий шаг - подключение библиотеки в компонент.

    import { GridStack } from 'gridstack';
    import 'gridstack/dist/gridstack.min.css';
  3. Далее, нам надо определиться со способом, которым мы будем загружать данные для отображения на сетке. Их всего два: 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 часа, а спать как бы хочется)

Надеюсь, что данная статья была для вас полезна! Всегда рад комментариям и обоснованной критике.

Всем мира <( ̄︶ ̄)>

Комментарии (2)


  1. shkery
    27.12.2023 10:42

    Крутая статья!я замотивирован.Автор лучший без сомнения!мотивацию надо подняяяяять!!!!!


    1. ShADAMoV Автор
      27.12.2023 10:42

      Спасибо)