Displaying data in lists is a core feature of most applications. Ginjou provides useList and useInfiniteList to make this easy.
useList is the go-to composable for fetching lists of data.
Composition:
currentPage, perPage, filters, and sorters.useGetList to fetch data based on the current state.useGo and useLocation to sync state with the URL (if enabled).<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>
<!-- WIP -->
<script>
// ...
</script>
For "Load More" or Infinite Scroll interfaces, use useInfiniteList.
Composition:
perPage (or limit), filters, and sorters. Note it does NOT use standard page-based pagination state like useList in the same way.useGetInfiniteList.<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>
<!-- WIP -->
<script>
// ...
</script>
useList provides currentPage, perPage, and pageCount refs.
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>
<!-- WIP -->
<script>
// ...
</script>
Updates to the filters array trigger data refetches (or client-side filtering).
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>
<!-- WIP -->
<script>
// ...
</script>
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>
<!-- WIP -->
<script>
// ...
</script>
Permanent filters are useful for:
Modify sorters to change order.
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>
<!-- WIP -->
<script>
// ...
</script>
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>
<!-- WIP -->
<script>
// ...
</script>
Permanent sorters are useful for:
syncRoute keeps your state in sync with the URL.
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
| Configuration | Result URL | UI Behavior |
|---|---|---|
false, undefined (default) | /posts | State 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=10 | Custom 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>
<!-- WIP -->
<script>
// ...
</script>
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=...
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:
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: