# ivyforms/1.0/frontend/src/composables/useTableUrlState.ts

The Innovative Form Builder – IvyForms, version 1.0. 364 lines.

- Page: https://pluginprobe.com/plugins/ivyforms/1.0/code/frontend/src/composables/useTableUrlState.ts
- Raw: https://pluginprobe.com/plugins/ivyforms/1.0/raw/frontend/src/composables/useTableUrlState.ts
- Modified: 2026-03-02T13:43:38+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ivyforms/1.0/code/frontend/src/composables/useTableUrlState.ts#L10-L20`.

```typescript
import { reactive, watch, type Ref, onMounted } from 'vue'
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'

export type TableFilterValue = string | number | boolean | null

export interface TableFilters {
  [key: string]: TableFilterValue
}

export interface TablePagination {
  page: number
  perPage: number
}

export interface TableSort {
  column: string | null
  order: 'asc' | 'desc' | null
}

export interface TableUrlStateOptions<TFilters extends TableFilters = TableFilters> {
  filters?: TFilters
  pagination?: TablePagination
  sort?: TableSort
  searchQuery?: Ref<string>
  dateRange?: Ref<[string, string] | null>
  preserveOtherParams?: boolean
  onRestored?: () => void
}

/**
 * Composable for syncing table state (filters, pagination, sorting, search) with URL query parameters
 * This ensures table state is preserved when navigating between pages
 */
export function useTableUrlState<TFilters extends TableFilters = TableFilters>(
  options: TableUrlStateOptions<TFilters> = {},
) {
  const route = useRoute()
  const router = useRouter()

  const filters = (options.filters ?? reactive({})) as TFilters
  const pagination = options.pagination ?? reactive({ page: 1, perPage: 10 })
  const sort = options.sort ?? reactive({ column: null, order: null })
  const searchQuery = options.searchQuery
  const dateRange = options.dateRange
  const preserveOtherParams = options.preserveOtherParams ?? true
  const onRestored = options.onRestored

  // Store default values
  const defaultPagination = { page: pagination.page, perPage: pagination.perPage }
  const defaultSort = { column: sort.column, order: sort.order }
  const defaultFilters = { ...filters }
  const mutableFilters = filters as TableFilters

  let isInitializing = true
  let skipNextUpdate = false
  let isRestoringFromUrl = false
  let isUpdatingUrl = false
  let suppressUpdates = false

  const replaceQuery = (query: LocationQueryRaw) => {
    isUpdatingUrl = true
    router.replace({ query }).finally(() => {
      isUpdatingUrl = false
    })
  }

  /**
   * Temporarily suppress URL updates for a block of code.
   * @param fn - The function to execute with updates suppressed
   */
  const withSuppressedUpdates = async (fn: () => Promise<void> | void) => {
    suppressUpdates = true
    try {
      await fn()
    } finally {
      suppressUpdates = false
    }
  }

  /**
   * Parse URL query parameters and restore table state
   */
  const restoreFromUrl = () => {
    isRestoringFromUrl = true
    const query = route.query

    // Restore pagination
    if (query.page) {
      const page = parseInt(query.page as string, 10)
      if (!isNaN(page) && page > 0) {
        pagination.page = page
      }
    } else {
      // Use default if not in URL
      pagination.page = defaultPagination.page
    }

    if (query.perPage) {
      const perPage = parseInt(query.perPage as string, 10)
      if (!isNaN(perPage) && perPage > 0) {
        pagination.perPage = perPage
      }
    } else {
      // Use default if not in URL
      pagination.perPage = defaultPagination.perPage
    }

    // Restore sorting
    if (query.sortColumn) {
      sort.column = query.sortColumn as string
    } else {
      // Use default if not in URL
      sort.column = defaultSort.column
    }

    if (query.sortOrder && (query.sortOrder === 'asc' || query.sortOrder === 'desc')) {
      sort.order = query.sortOrder as 'asc' | 'desc'
    } else {
      // Use default if not in URL
      sort.order = defaultSort.order
    }

    // Restore search query
    if (searchQuery) {
      if (query.search) {
        searchQuery.value = query.search as string
      } else {
        searchQuery.value = ''
      }
    }

    // Restore date range
    if (dateRange) {
      if (query.dateFrom && query.dateTo) {
        dateRange.value = [query.dateFrom as string, query.dateTo as string]
      } else {
        dateRange.value = null
      }
    }

    // Restore filters
    Object.keys(filters).forEach((key) => {
      const queryValue = query[key]
      if (queryValue !== undefined) {
        if (queryValue === 'true') {
          mutableFilters[key] = true
        } else if (queryValue === 'false') {
          mutableFilters[key] = false
        } else if (queryValue === 'null') {
          mutableFilters[key] = null
        } else {
          mutableFilters[key] = queryValue as TableFilterValue
        }
      } else {
        // Use default if not in URL
        mutableFilters[key] = defaultFilters[key] ?? null
      }
    })

    isRestoringFromUrl = false
    if (!isInitializing && onRestored) {
      onRestored()
    }
  }

  /**
   * Update URL with current table state
   */
  const updateUrl = () => {
    if (skipNextUpdate || isRestoringFromUrl || suppressUpdates) {
      skipNextUpdate = false
      return
    }

    const query: LocationQueryRaw = {}

    // Preserve other query params if needed
    if (preserveOtherParams) {
      Object.keys(route.query).forEach((key) => {
        // Don't preserve our managed params
        if (
          !['page', 'perPage', 'sortColumn', 'sortOrder', 'search', 'dateFrom', 'dateTo'].includes(
            key,
          ) &&
          !Object.keys(filters).includes(key)
        ) {
          query[key] = route.query[key]
        }
      })
    }

    // Add pagination
    if (pagination.page !== defaultPagination.page) {
      query.page = pagination.page.toString()
    }
    if (pagination.perPage !== defaultPagination.perPage) {
      query.perPage = pagination.perPage.toString()
    }

    // Add sorting
    if (sort.column && sort.column !== defaultSort.column) {
      query.sortColumn = sort.column
    }
    if (sort.order && sort.order !== defaultSort.order) {
      query.sortOrder = sort.order
    }

    // Add search query
    if (searchQuery?.value) {
      query.search = searchQuery.value
    }

    // Add date range
    if (dateRange?.value && Array.isArray(dateRange.value) && dateRange.value.length === 2) {
      query.dateFrom = dateRange.value[0]
      query.dateTo = dateRange.value[1]
    }

    // Add filters
    Object.keys(filters).forEach((key) => {
      const value = filters[key]
      if (value !== null && value !== undefined && value !== defaultFilters[key]) {
        query[key] = String(value)
      }
    })

    // Update URL without triggering navigation
    replaceQuery(query)
  }

  /**
   * Reset all table state and URL parameters
   */
  const resetState = () => {
    skipNextUpdate = true

    // Reset pagination
    pagination.page = 1
    pagination.perPage = 10

    // Reset sorting
    sort.column = null
    sort.order = null

    // Reset search
    if (searchQuery) {
      searchQuery.value = ''
    }

    // Reset date range
    if (dateRange) {
      dateRange.value = null
    }

    // Reset filters
    Object.keys(filters).forEach((key) => {
      mutableFilters[key] = null
    })

    // Clear URL params (preserve other params if needed)
    const query: LocationQueryRaw = {}
    if (preserveOtherParams) {
      Object.keys(route.query).forEach((key) => {
        if (
          !['page', 'perPage', 'sortColumn', 'sortOrder', 'search', 'dateFrom', 'dateTo'].includes(
            key,
          ) &&
          !Object.keys(filters).includes(key)
        ) {
          query[key] = route.query[key]
        }
      })
    }

    replaceQuery(query)
  }

  // Watch for route query changes (browser back/forward, direct navigation)
  watch(
    () => route.query,
    () => {
      if (isUpdatingUrl) {
        return
      }
      if (!isRestoringFromUrl && !isInitializing) {
        restoreFromUrl()
      }
    },
    { deep: true },
  )

  // Watch for changes and update URL (after router is ready)
  onMounted(() => {
    router.isReady().then(() => {
      restoreFromUrl()
      isInitializing = false
      if (onRestored) {
        onRestored()
      }

      // Watch pagination
      watch(
        () => [pagination.page, pagination.perPage],
        () => {
          if (!isInitializing && !isRestoringFromUrl) {
            updateUrl()
          }
        },
        { deep: true },
      )

      // Watch sorting
      watch(
        () => [sort.column, sort.order],
        () => {
          if (!isInitializing && !isRestoringFromUrl) {
            updateUrl()
          }
        },
        { deep: true },
      )

      // Watch search query
      if (searchQuery) {
        watch(searchQuery, () => {
          if (!isInitializing && !isRestoringFromUrl) {
            updateUrl()
          }
        })
      }

      // Watch date range
      if (dateRange) {
        watch(dateRange, () => {
          if (!isInitializing && !isRestoringFromUrl) {
            updateUrl()
          }
        })
      }

      // Watch filters
      watch(
        filters,
        () => {
          if (!isInitializing && !isRestoringFromUrl) {
            updateUrl()
          }
        },
        { deep: true },
      )
    })
  })

  return {
    filters,
    pagination,
    sort,
    resetState,
    updateUrl,
    restoreFromUrl,
    withSuppressedUpdates,
  }
}

```
