List
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, andsorters. - Data Hook: Uses
useGetListto fetch data based on the current state. - Router Sync: Uses
useGoanduseLocationto 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>
Infinite List
For "Load More" or Infinite Scroll interfaces, use useInfiniteList.
Composition:
- State Management: Manages
perPage(or limit),filters, andsorters. Note it does NOT use standard page-based pagination state likeuseListin the same way. - Data Hook: Uses
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>
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>
<!-- WIP -->
<script>
// ...
</script>
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>
<!-- WIP -->
<script>
// ...
</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>
<!-- WIP -->
<script>
// ...
</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>
<!-- WIP -->
<script>
// ...
</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>
<!-- WIP -->
<script>
// ...
</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.
syncRoute is highly recommended for list pages, as it allows users to bookmark or share specific search results and pagination states.<script setup lang="ts">
const { records } = useList({
resource: 'posts',
syncRoute: true,
})
// URL becomes: ?current=1&perPage=10...
</script>
<!-- WIP -->
<script>
// ...
</script>
Disable 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,
}
})
// URL: /posts?filters=...&sorters=...
Disable Filters Sync:
const { records } = useList({
resource: 'posts',
syncRoute: {
filters: false,
}
})
// URL: /posts?current=1&perPage=10&sorters=...
Disable Sorters Sync:
const { records } = useList({
resource: 'posts',
syncRoute: {
sorters: false,
}
})
// URL: /posts?current=1&perPage=10&filters=...
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
})
// URL: /posts
// Enable all sync with default field names
const { records } = useList({
resource: 'posts',
syncRoute: true, // Sync all state fields with default names
})
// URL: /posts?current=1&perPage=10&filters=...&sorters=...
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'
}
}
})
// URL: /posts?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'
}
}
})
// URL: /posts?page=1&limit=10&filters=...&sorters=...
Customize Sorter Parameter:
const { records } = useList({
resource: 'posts',
syncRoute: {
sorters: {
field: 'sort', // Changed from 'sorters' to 'sort'
}
}
})
// URL: /posts?current=1&perPage=10&filters=...&sort=...
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 }
})
}
}
}
})
// URL: /posts?search=status:published,author:john
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' }
})
}
}
}
})
// URL: /posts?sort=created_at-desc,title-asc