Guides

List

Displaying data lists.

Displaying data in lists is a core feature of most applications. Ginjou provides useList and useInfiniteList to make this easy.

List

useList is the go-to composable for fetching lists of data.

Composition:

  • State Management: Manages currentPage, perPage, filters, and sorters.
  • Data Hook: Uses useGetList to fetch data based on the current state.
  • Router Sync: Uses useGo and useLocation to sync state with the URL (if enabled).
mermaid
graph TD
    A[useList] --> B[useResource]
    A --> C[State Management]
    C --> |filters, sorters, pagination| D[useGetList]
    D --> |data, total| A
    C --> |syncRoute| E[useGo / useLocation]
    B -- Resource & Fetcher Name --> D
<script setup lang="ts">
import { useList } from '@ginjou/vue'

const { records, isFetching } = useList({
    resource: 'posts',
})
</script>

<template>
    <div v-if="isFetching">
        Loading...
    </div>
    <ul v-else>
        <li v-for="record in records" :key="record.id">
            {{ record.id }} - {{ record.title }}
        </li>
    </ul>
</template>

Infinite List

For "Load More" or Infinite Scroll interfaces, use useInfiniteList.

Composition:

  • State Management: Manages perPage (or limit), filters, and sorters. Note it does NOT use standard page-based pagination state like useList in the same way.
  • Data Hook: Uses useGetInfiniteList.
mermaid
graph TD
    A[useInfiniteList] --> B[useResource]
    A --> C[State Management]
    C --> |filters, sorters, perPage| D[useGetInfiniteList]
    D --> |data, hasNextPage| A
    C --> |syncRoute| E[useGo / useLocation]
    B -- Resource & Fetcher Name --> D
<script setup lang="ts">
import { useInfiniteList } from '@ginjou/vue'

const {
    records, // Note: This is a nested array of pages -> records
    hasNextPage,
    fetchNextPage,
    isFetching,
} = useInfiniteList({
    resource: 'posts',
    pagination: {
        perPage: 10,
    },
})
</script>

<template>
    <div v-for="(page, i) in records" :key="i">
        <div v-for="item in page" :key="item.id">
            {{ item.title }}
        </div>
    </div>

    <button
        v-if="hasNextPage"
        :disabled="isFetching"
        @click="fetchNextPage()"
    >
        {{ isFetching ? 'Loading...' : 'Load More' }}
    </button>
</template>

Pagination

useList provides currentPage, perPage, and pageCount refs.

Modes

You can control where pagination happens using pagination.mode:

  • server (Default): Parameters are sent to the API.
  • client: All data is expected to be available (or fetched once), and Ginjou slices the array in the browser.
  • off: Pagination is disabled.
<script setup lang="ts">
import { useList } from '@ginjou/vue'

const {
    records,
    currentPage,
    perPage,
    pageCount,
    total,
} = useList({
    resource: 'posts',
    pagination: {
        current: 1,
        perPage: 10,
        mode: 'server', // or 'client', 'off'
    }
})
</script>

<template>
    <!-- List rendering... -->

    <div class="pagination">
        <button :disabled="currentPage === 1" @click="currentPage--">
            Prev
        </button>
        <span>{{ currentPage }} / {{ pageCount }}</span>
        <button :disabled="currentPage === pageCount" @click="currentPage++">
            Next
        </button>
    </div>
</template>

Filters

Updates to the filters array trigger data refetches (or client-side filtering).

Modes

Controlled by filters.mode:

  • server (Default): Filters are sent to the API.
  • off: Filters are ignored/disabled.

(Note: Client-side filtering logic for useList is typically handled by the developer or specific helpers if mode: 'client' logic is needed, but the primary supported modes for the prop are server/off for API interaction)

<script setup lang="ts">
import { FilterOperator } from '@ginjou/core'
import { useList } from '@ginjou/vue'
import { reactive, unref, watch } from 'vue'

const { records, filters } = useList({
    resource: 'posts',
    filters: {
        mode: 'server',
    }
})

// ... form logic to update filters ...
</script>

Permanent

Use permanent to enforce constraints that users cannot remove. Permanent filters are always applied to queries, ensuring certain conditions are always met.

<script setup lang="ts">
import { FilterOperator } from '@ginjou/core'
import { useList } from '@ginjou/vue'

const { records } = useList({
    resource: 'posts',
    filters: {
        permanent: [
            {
                field: 'status',
                operator: FilterOperator.eq,
                value: 'published',
            }
        ],
        value: []
    }
})
</script>

Permanent filters are useful for:

  • Restricting lists to a specific user's data
  • Filtering to only active/published items
  • Enforcing multi-tenancy constraints
  • Applying organizational or role-based filters that users cannot override

Sorters

Modify sorters to change order.

Modes

Controlled by sorters.mode:

  • server (Default): Sort params sent to API.
  • off: Sorting disabled.
<script setup lang="ts">
import { useList } from '@ginjou/vue'

const { records, sorters } = useList({
    resource: 'posts',
    sorters: {
        mode: 'server',
        value: [
            { field: 'created_at', order: 'desc' },
        ],
    }
})
</script>

Permanent

Use permanent to enforce default sort orders that users cannot remove.

<script setup lang="ts">
import { useList } from '@ginjou/vue'

const { records, sorters } = useList({
    resource: 'posts',
    sorters: {
        permanent: [
            { field: 'created_at', order: 'desc' }
        ],
        value: []
    }
})
</script>

Permanent sorters are useful for:

  • Ensuring consistent default ordering (e.g., most recent first)
  • Combining multiple sort priorities
  • Maintaining required sort order while allowing users to add additional sorts

Sync With Route

syncRoute keeps your state in sync with the URL.

Enabling syncRoute is highly recommended for list pages, as it allows users to bookmark or share specific search results and pagination states.

Quick Reference: syncRoute Options

ConfigurationResult URLUI Behavior
false, undefined (default)/postsState is local, URL unchanged
true/posts?current=1&perPage=10&filters=[...]&sorters=[...]All state synced to URL
Disable pagination sync/posts?filters=[...]&sorters=[...]Only filters and sorters in URL
Custom field names/posts?page=1&limit=10Custom parameter names (e.g., page instead of current)
<script setup lang="ts">
const { records } = useList({
    resource: 'posts',
    syncRoute: true,
})
// URL becomes: ?current=1&perPage=10...
</script>

Disable Syncing Fields

By default, all state fields (pagination, filters, and sorters) are synchronized with the URL. You can selectively disable synchronization for specific fields to keep them as local state.

Disable Pagination Sync:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        currentPage: false,
        perPage: false,
    }
})

Result URL: ?filters=...&sorters=... (pagination fields excluded)

Disable Filters Sync:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        filters: false,
    }
})

Result URL: ?current=1&perPage=10&sorters=... (filters excluded)

Disable Sorters Sync:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        sorters: false,
    }
})

Result URL: ?current=1&perPage=10&filters=... (sorters excluded)

Disable All Sync:

By default, syncRoute is disabled (no synchronization). You can explicitly enable or disable it using a boolean value as a quick toggle.

// Disable all sync (default behavior when syncRoute is not set)
const { records } = useList({
    resource: 'posts',
    syncRoute: false, // All state is local, no URL parameters
})

Result URL: No query parameters

// Enable all sync with default field names
const { records } = useList({
    resource: 'posts',
    syncRoute: true, // Sync all state fields with default names
})

Result URL: ?current=1&perPage=10&filters=...&sorters=...

Custom Sync Field Names

Change the query parameter names to customize URL appearance. This is useful for SEO, shorter URLs, or maintaining compatibility with existing URL schemes.

Customize Filter Parameter:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        filters: {
            field: 'q', // Changed from 'filters' to 'q'
        }
    }
})

Result URL: ?current=1&perPage=10&q=...&sorters=...

Customize Pagination Parameter:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        currentPage: {
            field: 'page', // Changed from 'current'
        },
        perPage: {
            field: 'limit', // Changed from 'perPage'
        }
    }
})

Result URL: ?page=1&limit=10&filters=...&sorters=...

Customize Sorter Parameter:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        sorters: {
            field: 'sort', // Changed from 'sorters' to 'sort'
        }
    }
})

Result URL: ?current=1&perPage=10&filters=...&sort=...

Benefits of Custom Field Names:

  • Shorter, more readable URLs
  • SEO-friendly parameter names
  • Align with existing API or URL conventions
  • Improved user experience when sharing URLs

Custom Field Serialization/Deserialization

For complex filtering or sorting logic, you can customize how data is encoded to and decoded from URL query parameters using stringify and parse functions.

Custom Filters Serialization:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        filters: {
            field: 'search',
            stringify: (filters) => {
                // Convert filters to custom format: "status:published,author:john"
                return filters.map(f => `${f.field}:${f.value}`).join(',')
            },
            parse: (queryValue) => {
                // Parse custom format back to filter objects
                return queryValue.split(',').map((item) => {
                    const [field, value] = item.split(':')
                    return { field, operator: 'eq', value }
                })
            }
        }
    }
})

Result URL: ?search=status:published,author:john (custom format instead of encoded JSON)

Custom Sorters Serialization:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        sorters: {
            field: 'sort',
            stringify: (sorters) => {
                // Convert sorters to format: "created_at-desc,title-asc"
                return sorters.map(s => `${s.field}-${s.order}`).join(',')
            },
            parse: (queryValue) => {
                // Parse back to sorter objects
                return queryValue.split(',').map((item) => {
                    const [field, order] = item.split('-')
                    return { field, order: order as 'asc' | 'desc' }
                })
            }
        }
    }
})

Result URL: ?sort=created_at-desc,title-asc

Benefits of Custom Serialization:

  • Create human-readable URLs
  • Maintain compatibility with legacy URL formats
  • Reduce URL length with compact encoding
  • Support special filter operators beyond simple equality
  • Implement domain-specific URL structures