Skip to content

Data Binding

The Pantanal Grid enables you to bind it to different data sources. You can populate the Grid with local data arrays, remote data services (REST, GraphQL), WebSocket connections for real-time updates, or work offline with local storage.

Simple example with local data array. All operations (sorting, filtering, pagination) are performed client-side.

ID
Product Name
Unit Price
Units In Stock
Discontinued

Overview

Pantanal Grid supports multiple data binding strategies:

  • Local Data Arrays - Simple arrays for client-side operations
  • REST APIs - Standard HTTP endpoints with dataProvider or PantanalDataSource
  • GraphQL - GraphQL queries and mutations
  • WebSocket - Real-time data updates
  • Offline Mode - Local storage with sync capabilities
  • DataSource Component - Higher-level abstraction for data management

Binding to Local Data Arrays

The simplest way to provide data to the Grid is by using a local data array. All operations (sorting, filtering, pagination) are performed client-side.

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { PantanalGrid, type ColumnDef } from '@pantanal/grid'

const rows = ref<any[]>([])

const columns: ColumnDef[] = [
  { field: 'ProductID', title: 'ID', width: 80 },
  { field: 'ProductName', title: 'Product Name', width: 180 },
  { field: 'UnitPrice', title: 'Unit Price', width: 120, format: (v: number) => `$${v?.toFixed(2) || '0.00'}` },
  { field: 'UnitsInStock', title: 'Units In Stock', width: 120 },
  { field: 'Discontinued', width: 120 },
]

onMounted(() => {
  rows.value = [
    { "ProductID": 1, "ProductName": "Chai", "UnitPrice": 18, "UnitsInStock": 39, "Discontinued": false },
    { "ProductID": 2, "ProductName": "Chang", "UnitPrice": 17, "UnitsInStock": 40, "Discontinued": false },
    { "ProductID": 3, "ProductName": "Aniseed Syrup", "UnitPrice": 10, "UnitsInStock": 13, "Discontinued": false },
    { "ProductID": 4, "ProductName": "Chef Anton", "UnitPrice": 22, "UnitsInStock": 53, "Discontinued": false },
    { "ProductID": 5, "ProductName": "Chef Gumbo Mix", "UnitPrice": 21.35, "UnitsInStock": 0, "Discontinued": true },
    { "ProductID": 6, "ProductName": "Boysenberry Spread", "UnitPrice": 25, "UnitsInStock": 120, "Discontinued": false },
  ]
})
</script>

<template>
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    key-field="ProductID"
  />
</template>

Benefits:

  • Simple setup - just pass an array
  • Fast filtering and sorting - no server roundtrips
  • Works offline - no network dependency
  • Best for small to medium datasets (up to ~10,000 rows)

Binding to Remote Data Services (REST)

For remote data binding, use the dataProvider prop to fetch data from a REST API. The Grid handles pagination, sorting, and filtering requests automatically.

ID
Title
Brand
Price
Category
vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, type ColumnDef, type DataProvider, type DataProviderArgs } from '@pantanal/grid'

const rows = ref<any[]>([])
const loading = ref(false)

const columns: ColumnDef[] = [
  { field: 'id', title: 'ID', width: 80 },
  { field: 'title', title: 'Product Name', width: 180 },
  { field: 'price', title: 'Unit Price', width: 120, format: (v: number) => `$${v?.toFixed(2) || '0.00'}` },
  { field: 'stock', title: 'Units In Stock', width: 120 },
  { field: 'command', title: ' ', command: ['edit', 'destroy'], width: 170 },
]

const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  loading.value = true
  try {
    const { page, pageSize } = args
    const url = new URL('https://dummyjson.com/products')
    url.searchParams.set('limit', String(pageSize))
    url.searchParams.set('skip', String((page - 1) * pageSize))
    
    const res = await fetch(url.toString())
    const json = await res.json()
    
    return { rows: json.products, total: json.total }
  } catch (error) {
    console.error('Error:', error)
    return { rows: [], total: 0 }
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    :data-provider="dataProvider"
    :loading="loading"
    key-field="id"
    server-side
  />
</template>

Benefits:

  • Efficient for large datasets - only loads visible data
  • Server-side filtering and sorting - reduces client load
  • Supports any REST API endpoint
  • Automatic pagination handling

Binding to GraphQL Service

GraphQL services are served over HTTP through a single endpoint. The Grid can perform CRUD operations through GraphQL queries and mutations.

Note: This example simulates a GraphQL query. In a real application, you would use a GraphQL client library (like Apollo or urql) to execute queries against your GraphQL endpoint.

ID
Name
Email
Phone
vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, type ColumnDef, type DataProvider, type DataProviderArgs } from '@pantanal/grid'

const rows = ref<any[]>([])
const loading = ref(false)

const columns: ColumnDef[] = [
  { field: 'productID', title: 'Product ID', width: 110 },
  { field: 'productName', title: 'Product Name', width: 200 },
  { field: 'unitPrice', title: 'Unit Price', width: 120 },
  { field: 'unitsInStock', title: 'Units in Stock', width: 120 },
  { field: 'command', title: 'Options', width: 200, command: ['edit', 'destroy'] },
]

const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  loading.value = true
  try {
    const { page, pageSize } = args
    const query = `query {
      products(limit: ${pageSize}, skip: ${(page - 1) * pageSize}) {
        productID, productName, unitPrice, unitsInStock
      }
    }`
    
    const res = await fetch('https://your-graphql-endpoint.com/graphql', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query }),
    })
    
    const json = await res.json()
    return { rows: json.data?.products || [], total: json.data?.total || 0 }
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    :data-provider="dataProvider"
    :loading="loading"
    key-field="productID"
    server-side
  />
</template>

Benefits:

  • Single endpoint for all operations
  • Flexible querying - request only needed fields
  • Type-safe with GraphQL schema
  • Supports subscriptions for real-time updates

Binding to WebSocket (Real-time Updates)

The Grid allows you to perform real-time updates for all users. You can utilize WebSockets to receive push notifications when data changes on the server.

○ Disconnected (Simulated - updates every 3 seconds)
ID
Product Name
Price
Last Updated

Note: This example simulates WebSocket updates. In a real application, you would connect to an actual WebSocket server:

const ws = new WebSocket('ws://your-server.com')
ws.onmessage = (event) => {
  const data = JSON.parse(event.data)
  rows.value = data.rows || []
}
vue
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { PantanalGrid, type ColumnDef } from '@pantanal/grid'

const rows = ref<any[]>([])
let ws: WebSocket | null = null

const columns: ColumnDef[] = [
  { field: 'ProductName', title: 'Product Name', width: 200 },
  { field: 'UnitPrice', title: 'Unit Price', width: 120 },
  { field: 'command', title: ' ', width: 100, command: ['destroy'] },
]

onMounted(() => {
  const host = 'wss://your-websocket-server.com'
  ws = new WebSocket(host)
  
  ws.onopen = () => {
    ws?.send(JSON.stringify({ type: 'read' }))
  }
  
  ws.onmessage = (event) => {
    const result = JSON.parse(event.data)
    if (result.type === 'push-create' || result.type === 'push-update') {
      // Update rows with real-time data
      rows.value = result.data
    } else if (result.type === 'push-destroy') {
      rows.value = rows.value.filter((item: any) => item.ProductID !== result.data.ProductID)
    }
  }
})

onBeforeUnmount(() => {
  if (ws) ws.close()
})
</script>

<template>
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    key-field="ProductID"
  />
</template>

Benefits:

  • Real-time synchronization across clients
  • Push notifications for data changes
  • Efficient for collaborative applications
  • Low latency updates

Working Offline

The Grid supports working offline by storing data in localStorage. You can edit, create, or delete records when offline, and sync the updates when you're back online.

vue
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { PantanalGrid, type ColumnDef } from '@pantanal/grid'

const rows = ref<any[]>([])
const isOnline = ref(navigator.onLine)

const columns: ColumnDef[] = [
  { field: 'ProductName', title: 'Product Name', width: 200 },
  { field: 'UnitPrice', title: 'Unit Price', width: 120 },
  { field: 'UnitsInStock', title: 'Units In Stock', width: 120 },
  { field: 'command', title: ' ', width: 170, command: ['edit', 'destroy'] },
]

onMounted(() => {
  // Load from localStorage
  const stored = localStorage.getItem('offline-products')
  if (stored) {
    rows.value = JSON.parse(stored)
  }
  
  window.addEventListener('online', syncData)
})

// Save to localStorage on changes
watch(rows, () => {
  localStorage.setItem('offline-products', JSON.stringify(rows.value))
}, { deep: true })

function syncData() {
  // Sync pending changes to server
  const pending = localStorage.getItem('offline-pending')
  if (pending) {
    const changes = JSON.parse(pending)
    // Send to server...
    localStorage.removeItem('offline-pending')
  }
}
</script>

<template>
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    key-field="ProductID"
    editable="inline"
    toolbar="['create']"
  />
</template>

Benefits:

  • Works without internet connection
  • Automatic local storage persistence
  • Sync when connection is restored
  • Better user experience in unreliable networks

Data Provider Pattern

The dataProvider prop allows you to provide a function that fetches data asynchronously. The grid automatically calls this function when pagination, sorting, or filtering changes.

Basic Data Provider

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, type ColumnDef, type DataProvider, type DataProviderArgs } from '@pantanal/grid'

const loading = ref(false)

const columns: ColumnDef[] = [
  { field: 'id', title: 'ID', width: 80 },
  { field: 'name', title: 'Name', width: 200 },
  { field: 'price', title: 'Price', width: 120, format: (v: number) => `$${v.toFixed(2)}` }
]

const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  loading.value = true
  try {
    const { page, pageSize, sort, filter, signal } = args
    
    // Build API URL with parameters
    const url = new URL('https://api.example.com/products')
    url.searchParams.set('page', String(page))
    url.searchParams.set('limit', String(pageSize))
    
    if (sort && sort.length > 0) {
      url.searchParams.set('sort', JSON.stringify(sort))
    }
    
    if (filter && filter.length > 0) {
      url.searchParams.set('filter', JSON.stringify(filter))
    }
    
    const response = await fetch(url.toString(), { signal })
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    
    const data = await response.json()
    
    return {
      rows: data.items || [],
      total: data.total || 0
    }
  } catch (error) {
    console.error('Data fetch error:', error)
    return { rows: [], total: 0 }
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <PantanalGrid
    :rows="[]"
    :columns="columns"
    :data-provider="dataProvider"
    :loading="loading"
    :auto-bind="true"
    key-field="id"
    server-side
  />
</template>

Data Provider with Auto-bind

Use auto-bind="true" to automatically load data when the component mounts:

vue
<PantanalGrid
  :rows="[]"
  :columns="columns"
  :data-provider="dataProvider"
  :loading="loading"
  :auto-bind="true"
  key-field="id"
  server-side
/>

Manual Data Binding

You can also trigger data loading manually:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid } from '@pantanal/grid'

const gridRef = ref<InstanceType<typeof PantanalGrid>>()

async function loadData() {
  if (gridRef.value) {
    await gridRef.value.rebind()
  }
}
</script>

<template>
  <div>
    <button @click="loadData">Load Data</button>
    <PantanalGrid
      ref="gridRef"
      :rows="[]"
      :columns="columns"
      :data-provider="dataProvider"
      key-field="id"
      server-side
    />
  </div>
</template>

PantanalDataSource Component

The PantanalDataSource component provides a higher-level abstraction for data management. It handles filtering, sorting, and pagination automatically.

Local DataSource

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, PantanalDataSource, type ColumnDef, type DataSourceInstance } from '@pantanal/grid'

const dataSourceRef = ref<DataSourceInstance | null>(null)
const data = ref([/* your data */])
const rows = ref<any[]>([])
const page = ref(1)
const pageSize = ref(10)
const sort = ref([])
const filter = ref([])

const columns: ColumnDef[] = [/* ... */]

function handleChange(data: any[]) {
  rows.value = data
}
</script>

<template>
  <PantanalDataSource
    ref="dataSourceRef"
    type="local"
    :data="data"
    v-model:page="page"
    v-model:pageSize="pageSize"
    v-model:sort="sort"
    v-model:filter="filter"
    @change="handleChange"
  />
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    key-field="id"
    v-model:page="page"
    v-model:pageSize="pageSize"
    v-model:sort="sort"
    v-model:filter="filter"
  />
</template>

Remote DataSource with Transport

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, PantanalDataSource, type ColumnDef, type DataSourceInstance, type DataSourceTransport, type DataSourceSchema } from '@pantanal/grid'

const dataSourceRef = ref<DataSourceInstance | null>(null)
const rows = ref<any[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const pageSize = ref(20)

const columns: ColumnDef[] = [/* ... */]

const transport: DataSourceTransport = {
  read: async (options: any) => {
    const params = options.data || {}
    const url = new URL('https://api.example.com/data')
    url.searchParams.set('page', String(params.page || 1))
    url.searchParams.set('limit', String(params.pageSize || 20))
    
    const res = await fetch(url.toString(), { signal: options.signal })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json()
  },
  parameterMap: (data: any) => {
    return {
      page: page.value,
      pageSize: pageSize.value,
      ...data
    }
  }
}

const schema: DataSourceSchema = {
  data: (response: any) => response.items || [],
  total: (response: any) => response.total || 0
}

function handleChange(data: any[]) {
  rows.value = data
  if (dataSourceRef.value) {
    total.value = dataSourceRef.value.total()
  }
}
</script>

<template>
  <PantanalDataSource
    ref="dataSourceRef"
    type="remote"
    :transport="transport"
    :schema="schema"
    :server-paging="true"
    :server-sorting="true"
    :server-filtering="true"
    v-model:page="page"
    v-model:pageSize="pageSize"
    @change="handleChange"
    @requestStart="loading = true"
    @requestEnd="loading = false"
  />
  <PantanalGrid
    :rows="rows"
    :columns="columns"
    :total="total"
    key-field="id"
    server-side
    v-model:page="page"
    v-model:pageSize="pageSize"
    :loading="loading"
  />
</template>

Data Provider Arguments

The DataProviderArgs interface provides all the information needed to fetch data:

typescript
interface DataProviderArgs {
  page: number              // Current page number
  pageSize: number          // Number of rows per page
  sort?: SortDescriptor[]   // Current sort configuration
  filter?: FilterDescriptor[] // Current filter configuration
  group?: GroupDescriptor[] // Current group configuration
  signal?: AbortSignal      // AbortSignal for request cancellation
}

Data Provider Response

Your data provider function must return an object with this structure:

typescript
interface DataProviderResult {
  rows: any[]    // Array of row data
  total: number  // Total number of rows (for pagination)
}

Request Cancellation

The grid automatically cancels previous requests when new ones are made using AbortSignal:

typescript
const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  const { signal } = args
  
  const response = await fetch(url, { signal })
  // If the request is cancelled, fetch will throw an AbortError
  // which you can handle gracefully
}

Error Handling

Always implement proper error handling for remote data sources:

typescript
const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  try {
    const response = await fetch(url, { signal: args.signal })
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    const data = await response.json()
    return { rows: data.items || [], total: data.total || 0 }
  } catch (error: any) {
    if (error.name === 'AbortError') {
      // Request was cancelled, return empty result
      return { rows: [], total: 0 }
    }
    
    console.error('Data fetch error:', error)
    // Show user-friendly error message
    // You could emit an event or show a toast notification here
    return { rows: [], total: 0 }
  }
}

Loading States

Manage loading states to provide user feedback:

vue
<script setup lang="ts">
const loading = ref(false)

const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  loading.value = true
  try {
    // ... fetch data
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <PantanalGrid
    :rows="[]"
    :columns="columns"
    :data-provider="dataProvider"
    :loading="loading"
    :auto-bind="true"
    key-field="id"
    server-side
  />
</template>

Best Practices

Choosing the Right Data Source

  • Local Arrays: Best for small datasets (< 10,000 rows) or static data
  • REST API with dataProvider: Best for standard CRUD operations with large datasets
  • REST API with DataSource: Best when you need more control over data operations
  • GraphQL: Best for complex queries and type-safe APIs
  • WebSocket: Best for real-time collaborative applications
  • Offline Mode: Best for mobile apps or unreliable network conditions

Performance Tips

  1. Local Arrays: Use virtual scrolling for large local datasets (10,000+ rows)
  2. REST API: Implement proper caching and request debouncing
  3. GraphQL: Request only the fields you need to reduce payload size
  4. WebSocket: Use connection pooling and reconnection logic
  5. Offline Mode: Batch sync operations to reduce server load
  6. Request Cancellation: Always use the signal parameter to cancel stale requests
  7. Error Handling: Provide user-friendly error messages and fallback UI

Data Source Comparison

FeatureLocal ArraydataProviderDataSource Component
Setup ComplexityLowMediumHigh
Client-side Operations✅ Full❌ Limited✅ Full
Server-side Operations✅ Full✅ Full
State ManagementManualAutomaticAutomatic
Error HandlingManualManualBuilt-in events
Request CancellationN/A✅ Automatic✅ Automatic
Best ForSmall datasetsSimple APIsComplex scenarios

DataSource Component Features

The PantanalDataSource component provides additional features beyond basic data binding:

DataSource Events

vue
<PantanalDataSource
  ref="dataSourceRef"
  type="remote"
  :transport="transport"
  :schema="schema"
  @change="handleChange"           // Fired when data changes
  @error="handleError"             // Fired on data fetch errors
  @requestStart="loading = true"  // Fired when request starts
  @requestEnd="loading = false"    // Fired when request ends
/>

DataSource Methods

vue
<script setup lang="ts">
const dataSourceRef = ref<DataSourceInstance | null>(null)

// Read data manually
async function loadData() {
  if (dataSourceRef.value) {
    await dataSourceRef.value.read()
  }
}

// Get total count
function getTotal() {
  return dataSourceRef.value?.total() || 0
}

// Get current data
function getData() {
  return dataSourceRef.value?.data() || []
}

// Refresh data
async function refresh() {
  if (dataSourceRef.value) {
    await dataSourceRef.value.read()
  }
}
</script>

DataSource Configuration

vue
<PantanalDataSource
  type="remote"
  :transport="transport"
  :schema="schema"
  :server-paging="true"      // Enable server-side pagination
  :server-sorting="true"     // Enable server-side sorting
  :server-filtering="true"   // Enable server-side filtering
  :server-grouping="true"    // Enable server-side grouping
  :auto-sync="true"          // Auto-load on mount (default: true)
  :batch="false"             // Batch operations
/>

Advanced Patterns

Combining Multiple Data Sources

You can switch between different data sources dynamically:

vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { PantanalGrid, type ColumnDef, type DataProvider } from '@pantanal/grid'

const dataSourceType = ref<'local' | 'rest' | 'graphql'>('local')
const localData = ref([/* ... */])
const loading = ref(false)

const dataProvider: DataProvider<any> = computed(() => {
  if (dataSourceType.value === 'local') {
    return async () => ({ rows: localData.value, total: localData.value.length })
  }
  
  if (dataSourceType.value === 'rest') {
    return async (args) => {
      // REST API implementation
      const res = await fetch(`/api/data?page=${args.page}`)
      const data = await res.json()
      return { rows: data.items, total: data.total }
    }
  }
  
  // GraphQL implementation
  return async (args) => {
    // GraphQL query implementation
  }
})
</script>

<template>
  <div>
    <select v-model="dataSourceType">
      <option value="local">Local Data</option>
      <option value="rest">REST API</option>
      <option value="graphql">GraphQL</option>
    </select>
    
    <PantanalGrid
      :rows="[]"
      :columns="columns"
      :data-provider="dataProvider"
      :loading="loading"
      :auto-bind="true"
      key-field="id"
      server-side
    />
  </div>
</template>

Caching Data Provider Results

Implement caching to reduce API calls:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, type DataProvider, type DataProviderArgs } from '@pantanal/grid'

const cache = new Map<string, { rows: any[], total: number, timestamp: number }>()
const CACHE_TTL = 60000 // 1 minute

const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  const cacheKey = JSON.stringify({ page: args.page, pageSize: args.pageSize, sort: args.sort, filter: args.filter })
  const cached = cache.get(cacheKey)
  
  // Return cached data if still valid
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return { rows: cached.rows, total: cached.total }
  }
  
  // Fetch fresh data
  const response = await fetch(`/api/data?${new URLSearchParams({
    page: String(args.page),
    limit: String(args.pageSize)
  })}`)
  const data = await response.json()
  
  // Cache the result
  cache.set(cacheKey, {
    rows: data.items,
    total: data.total,
    timestamp: Date.now()
  })
  
  return { rows: data.items, total: data.total }
}
</script>

Debouncing Data Provider Calls

Debounce rapid changes to reduce API calls:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PantanalGrid, type DataProvider, type DataProviderArgs } from '@pantanal/grid'

let debounceTimer: number | null = null
const DEBOUNCE_DELAY = 300 // milliseconds

const dataProvider: DataProvider<any> = async (args: DataProviderArgs) => {
  return new Promise((resolve) => {
    if (debounceTimer) {
      clearTimeout(debounceTimer)
    }
    
    debounceTimer = window.setTimeout(async () => {
      const response = await fetch(`/api/data?page=${args.page}`)
      const data = await response.json()
      resolve({ rows: data.items, total: data.total })
    }, DEBOUNCE_DELAY)
  })
}
</script>

TypeScript Support

All data binding features are fully typed:

typescript
import {
  PantanalGrid,
  PantanalDataSource,
  type ColumnDef,
  type DataProvider,
  type DataProviderArgs,
  type DataSourceInstance,
  type DataSourceTransport,
  type DataSourceSchema
} from '@pantanal/grid'

// Type-safe data provider
const dataProvider: DataProvider<MyDataType> = async (args: DataProviderArgs) => {
  // args is fully typed
  // Return type is inferred
  return { rows: [], total: 0 }
}