Layout Persistence
The layout persistence feature allows you to save, load, and version control your layouts using Supabase as a backend. This guide explains how to set up and use the layout persistence system.
Setup
1. Database Setup
First, you need to set up the required tables in your Supabase database:
-- Create layout states table
create table public.layout_states (
id uuid not null default gen_random_uuid(),
state_id text not null,
layout jsonb not null default '{}'::jsonb,
created_at timestamp with time zone not null default timezone('utc'::text, now()),
updated_at timestamp with time zone not null default timezone('utc'::text, now()),
constraint layout_states_pkey primary key (id),
constraint layout_states_state_id_key unique (state_id)
);
-- Create layout versions table
create table public.layout_state_versions (
id uuid not null default gen_random_uuid(),
state_id uuid not null,
version_name text not null,
layout jsonb not null default '{}'::jsonb,
created_at timestamp with time zone not null default timezone('utc'::text, now()),
constraint layout_state_versions_pkey primary key (id),
constraint layout_state_versions_state_id_fkey foreign key (state_id)
references layout_states (id)
on delete cascade
);
-- Add indexes for better performance
create index idx_layout_states_state_id on public.layout_states(state_id);
create index idx_layout_state_versions_state_id on public.layout_state_versions(state_id);
create index idx_layout_state_versions_created_at on public.layout_state_versions(created_at desc);
2. Initialize the Composable
Import and initialize the useLayoutPersistence
composable in your component:
import { useLayoutPersistence } from '@edanweis/vue-code-layout'
import { supabase } from './supabase-client'
const layoutState = useLayoutPersistence({
supabase, // Configured Supabase client
stateId: 'my-layout', // Optional: Unique identifier for this layout
layoutInstance: layoutRef, // Reference to the layout component
autoSync: true, // Optional: Auto-save changes (default: true)
autoSyncDebounce: 1000, // Optional: Debounce delay in ms (default: 1000)
onError: console.error // Optional: Error handler
})
Usage
Basic State Management
// Load initial state
await layoutState.loadState()
// Save current state
await layoutState.saveState()
Version Management
// Create new version
await layoutState.createVersion('Version 1')
// Load specific version
await layoutState.loadVersion(version)
// Get list of versions
const versions = layoutState.versions
State ID Management
State IDs can be provided in several ways:
URL Parameters:
https://your-app.com/layout?stateId=my-layout-1
Direct Configuration:
typescriptconst layoutState = useLayoutPersistence({ stateId: 'my-layout-1', // ... other options })
Local Storage: The last used state ID is automatically saved to localStorage and restored on page load.
Advanced Configuration
const layoutState = useLayoutPersistence({
// Custom table names
tables: {
states: 'custom_layout_states',
versions: 'custom_layout_versions'
},
// Custom column names
columns: {
stateId: 'state_id',
layout: 'layout_data',
versionName: 'version_name',
createdAt: 'created_at',
updatedAt: 'updated_at'
},
// Additional data to include when saving
additionalData: {
workspace_id: '123',
team_id: '456'
},
// Or use a function for dynamic values
additionalData: () => ({
workspace_id: currentWorkspace.value.id,
last_modified_by: currentUser.value.id
})
})
Example Component
Here's a complete example showing how to implement layout persistence with version control:
<template>
<div class="layout-container">
<!-- Layout Controls -->
<div class="controls">
<button @click="handleSaveState">Save State</button>
<button @click="handleCreateVersion">Create Version</button>
<!-- Version Selector -->
<div class="version-selector">
<select v-model="selectedVersion">
<option value="">Select Version</option>
<option v-for="version in versions" :key="version.id" :value="version">
{{ formatVersionName(version) }}
</option>
</select>
<button v-if="selectedVersion" @click="handleLoadVersion">
Load Version
</button>
</div>
</div>
<!-- Layout Component -->
<SplitLayout ref="layoutRef" @layout-change="handleLayoutChange" />
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useLayoutPersistence } from '@edanweis/vue-code-layout'
const layoutRef = ref()
const versions = ref([])
const selectedVersion = ref(null)
const layoutState = useLayoutPersistence({
supabase,
layoutInstance: layoutRef,
autoSync: true
})
// Initialize
await layoutState.loadState()
// Watch for version changes
watch(() => layoutState.versions, newVersions => {
versions.value = newVersions
})
// Version management
const handleCreateVersion = async () => {
const versionName = `Version ${new Date().toLocaleTimeString()}`
await layoutState.createVersion(versionName)
}
const handleLoadVersion = async () => {
if (selectedVersion.value) {
await layoutState.loadVersion(selectedVersion.value)
}
}
</script>
Best Practices
State IDs:
- Use meaningful state IDs to identify layout purposes
- Consider using URL parameters for easy sharing
- Store commonly used state IDs in localStorage
Version Control:
- Create versions after significant changes
- Use descriptive version names
- Consider including timestamps in version names
Error Handling:
- Always provide an error handler
- Handle loading states appropriately
- Show success/failure feedback to users
Performance:
- Use appropriate auto-sync debounce values
- Consider disabling auto-sync in performance-critical applications
- Utilize the provided database indexes
TypeScript Support
The composable includes full TypeScript support with the following interfaces:
interface LayoutPersistenceState {
id: string // UUID
state_id: string // User-provided identifier
layout: any // Layout data
created_at: string
updated_at: string
}
interface LayoutPersistenceVersion {
id: string // UUID
state_id: string // References layout_states.id
version_name: string
layout: any // Layout data
created_at: string
}
interface UseLayoutPersistenceOptions {
supabase: SupabaseClient // Pre-configured Supabase client
stateId?: string // Optional: Unique identifier for layout state
layoutInstance: any // Layout component instance
autoSync?: boolean // Whether to auto-sync (default: true)
autoSyncDebounce?: number // Debounce delay for auto-sync in ms (default: 1000)
onError?: (error: any) => void // Error handler callback
tables?: { // Custom table names
states?: string
versions?: string
}
columns?: { // Custom column names
stateId?: string
layout?: string
versionName?: string
createdAt?: string
updatedAt?: string
}
additionalData?: Record<string, any> | (() => Record<string, any>) // Additional data to include when saving states
additionalVersionData?: Record<string, any> | (() => Record<string, any>) // Additional data to include when saving versions
}
Events
When using the layout store plugin, you can define event handlers that will be called before and after various operations. This allows you to perform custom actions at specific points in the layout persistence lifecycle.
Available Events
interface LayoutStoreEvents {
// Called before saving the layout state
onBeforeSave?: () => void | Promise<void>
// Called after successfully saving the layout state
onAfterSave?: (state: LayoutPersistenceState) => void | Promise<void>
// Called before loading a layout state
onBeforeLoad?: () => void | Promise<void>
// Called after successfully loading a layout state
onAfterLoad?: (state: LayoutPersistenceState) => void | Promise<void>
// Called before creating a new version
onBeforeCreateVersion?: () => void | Promise<void>
// Called after successfully creating a new version
onAfterCreateVersion?: (version: LayoutPersistenceVersion) => void | Promise<void>
// Called before loading a version
onBeforeLoadVersion?: (version: LayoutPersistenceVersion) => void | Promise<void>
// Called after successfully loading a version
onAfterLoadVersion?: (version: LayoutPersistenceVersion) => void | Promise<void>
// Called when an error occurs in any operation
onError?: (error: any) => void | Promise<void>
}
Example Usage
import { createLayoutStore } from '@edanweis/vue-code-layout'
import { supabase } from './supabase-client'
const layoutStore = createLayoutStore()
// Initialize with events
await layoutStore.initialize({
supabase,
stateId: 'my-layout',
events: {
onBeforeSave: () => {
console.log('About to save layout state...')
},
onAfterSave: (state) => {
console.log('Layout state saved:', state)
// You could show a notification here
},
onBeforeLoad: () => {
console.log('About to load layout state...')
// You could show a loading indicator
},
onAfterLoad: (state) => {
console.log('Layout state loaded:', state)
// You could hide the loading indicator
},
onBeforeCreateVersion: () => {
console.log('Creating new version...')
},
onAfterCreateVersion: (version) => {
console.log('New version created:', version)
// You could show a success message
},
onError: (error) => {
console.error('Layout operation failed:', error)
// You could show an error notification
}
}
})
Best Practices for Events
Error Handling:
- Always implement the
onError
event handler to handle and display errors appropriately - Use try-catch blocks in your event handlers if performing complex operations
- Always implement the
Async Operations:
- All event handlers can be asynchronous (return a Promise)
- Use async/await for cleaner code when performing async operations
- Be careful not to block the UI thread with long-running operations
State Management:
- Use events to sync layout state with other parts of your application
- Consider integrating with your application's notification system
- Update loading states and UI feedback in event handlers
Performance:
- Keep event handlers light and fast
- Move heavy computations to web workers if necessary
- Consider debouncing or throttling if events are triggered frequently
Example with Loading States
<template>
<div class="layout-container">
<div v-if="isLoading" class="loading-overlay">
Loading layout...
</div>
<SplitLayout ref="layoutRef" />
<div class="notifications">
<div v-if="notification" :class="notification.type">
{{ notification.message }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useLayoutStore } from '@edanweis/vue-code-layout'
const layoutRef = ref()
const isLoading = ref(false)
const notification = ref<{ type: string; message: string } | null>(null)
const showNotification = (message: string, type: string = 'info') => {
notification.value = { message, type }
setTimeout(() => {
notification.value = null
}, 3000)
}
const store = useLayoutStore()
// Set up layout instance
store.setLayoutInstance(layoutRef)
// Initialize with events
await store.initialize({
supabase,
events: {
onBeforeLoad: () => {
isLoading.value = true
},
onAfterLoad: () => {
isLoading.value = false
showNotification('Layout loaded successfully', 'success')
},
onBeforeSave: () => {
showNotification('Saving layout...', 'info')
},
onAfterSave: () => {
showNotification('Layout saved successfully', 'success')
},
onError: (error) => {
isLoading.value = false
showNotification(error.message, 'error')
}
}
})
</script>
<style scoped>
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.notifications {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.notifications > div {
padding: 10px 20px;
border-radius: 4px;
margin-bottom: 10px;
}
.info {
background: #2196f3;
color: white;
}
.success {
background: #4caf50;
color: white;
}
.error {
background: #f44336;
color: white;
}
</style>