Skip to content

Common Tasks

These recipes show the ownership and success conditions that matter in v2. Start with the shortest path that matches your renderer.

Render a controlled grid

Keep the Layout in a writable ref and bind it with v-model:layout. Each item gets its coordinates and constraints from that array.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { GridLayout } from 'grid-layout-plus'

import type { Layout } from 'grid-layout-plus'

const layout = ref<Layout>([
  { i: 'summary', x: 0, y: 0, w: 4, h: 2, minW: 2 },
  { i: 'activity', x: 4, y: 0, w: 8, h: 3 },
])
</script>

<template>
  <GridLayout v-model:layout="layout" :col-num="12" :row-height="32">
    <template #item="{ item, isDragging, isResizing }">
      <article>
        <strong>{{ item.i }}</strong>
        <span v-if="isDragging || isResizing">Updating…</span>
      </article>
    </template>
  </GridLayout>
</template>

Do not pass x, y, w, h, or static to GridItem. Those deprecated compatibility props are not the source of truth inside GridLayout.

Keep responsive Layouts synchronized

Responsive mode controls two values: the current Layout and the map of breakpoint Layouts. Bind both models so Grid Layout Plus can propose them in the same Vue update cycle.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { GridLayout } from 'grid-layout-plus'

import type { Breakpoints, Layout, ResponsiveLayoutsInput } from 'grid-layout-plus'

type AppBreakpoint = 'mobile' | 'desktop'

const breakpoints = { mobile: 0, desktop: 1024 } satisfies Breakpoints<AppBreakpoint>
const cols = { mobile: 4, desktop: 12 } as const
const responsiveLayouts = ref<ResponsiveLayoutsInput<AppBreakpoint>>({
  mobile: [{ i: 'summary', x: 0, y: 0, w: 4, h: 2 }],
  desktop: [{ i: 'summary', x: 0, y: 0, w: 4, h: 2 }],
})
const layout = ref<Layout>(responsiveLayouts.value.desktop?.map(item => ({ ...item })) ?? [])
</script>

<template>
  <GridLayout
    v-model:layout="layout"
    v-model:responsive-layouts="responsiveLayouts"
    responsive
    :breakpoints="breakpoints"
    :cols="cols"
  >
    <template #item="{ item }">{{ item.i }}</template>
  </GridLayout>
</template>

Do not update only one model after width resolution. A partial update is rejected with GridLayoutRuntimeError.code === 'partial-responsive-update', and the last valid pair is restored.

Run a programmatic component command

An accepted component method returns pending, emits a controlled proposal, and waits for the parent to write it back. Use layout-updated to observe the commit.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { GridLayout } from 'grid-layout-plus'

import type {
  GridLayoutExpose,
  Layout,
  LayoutUpdateMeta,
  OperationRejectedPayload,
  ReadonlyLayout,
} from 'grid-layout-plus'

const grid = ref<GridLayoutExpose | null>(null)
const layout = ref<Layout>([{ i: 'summary', x: 0, y: 0, w: 4, h: 2 }])
const pendingRevision = ref<number | null>(null)

function moveSummary() {
  const receipt = grid.value?.moveItem('summary', 4, 0)
  pendingRevision.value = receipt?.status === 'pending' ? receipt.revision : null
}

function handleLayoutUpdated(_layout: ReadonlyLayout, meta: LayoutUpdateMeta) {
  if (meta.revision === pendingRevision.value) pendingRevision.value = null
}

function handleRejected(payload: OperationRejectedPayload) {
  if (payload.revision === pendingRevision.value) pendingRevision.value = null
  console.warn(payload.reason)
}
</script>

<template>
  <button type="button" @click="moveSummary">Move summary</button>
  <GridLayout
    ref="grid"
    v-model:layout="layout"
    @layout-updated="handleLayoutUpdated"
    @operation-rejected="handleRejected"
  />
</template>

pending does not mean committed. The proposal can still be rejected as external-not-committed or superseded by a newer command.

Manage Layout state without rendering components

Pass a writable Ref<Layout> to useGridLayout when a custom renderer owns the DOM. One-off operations commit synchronously, so accepted means the ref already contains the new Layout.

ts
import { ref } from 'vue'
import { useGridLayout } from 'grid-layout-plus'

import type { Layout } from 'grid-layout-plus'

const sourceLayout = ref<Layout>([{ i: 'summary', x: 0, y: 0, w: 4, h: 2 }])

const grid = useGridLayout({
  layout: sourceLayout,
  cols: 12,
  rowHeight: 32,
  margin: [8, 8],
  onOperationRejected(payload) {
    console.warn(payload.reason)
  },
})

const result = grid.moveItem('summary', 4, 0)
if (result.status === 'accepted') {
  console.log('Committed Layout:', grid.layout.value)
}

If you pass a plain ReadonlyLayout instead of a ref, the composable owns an internal writable copy. The ownership mode is fixed when the composable is created.

Validate and normalize Layout data without Vue

Use the /core entry for server-side validation, persistence boundaries, workers, or algorithm-only code. It does not inject component CSS.

ts
import {
  GridLayoutValidationError,
  normalizeLayout,
  validateLayout,
} from 'grid-layout-plus/core'

import type { Layout, ReadonlyLayout } from 'grid-layout-plus/core'

function prepareSavedLayout(layout: ReadonlyLayout): Layout {
  try {
    validateLayout(layout, 'savedLayout')

    return normalizeLayout(layout, {
      cols: 12,
      maxRows: 40,
      collisionMode: 'push',
    })
  } catch (error) {
    if (error instanceof GridLayoutValidationError) {
      console.error(error.code, error.path, error.cause)
    }
    throw error
  }
}

Validation checks the public data contract. Normalization then applies columns, maximum rows, collision handling, placement, and compaction, returning an independent mutable Layout.

Where to go next

Released under the MIT License.