| [ Web Proxy ] |
| Viewing: https://adminforth.dev/docs/tutorial/Customization/afcl/dataDisplay/ | [Back] [Original] |
Tables and other views of data, and indicators of loading and progress. Import the components from @/afcl, see AdminForth Components Library.
import { Table } from '@/afcl'
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'UK' },
{ name: 'Colin', age: 40, country: 'AU' },
]"
></Table>
import { Table } from '@/afcl'
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'UK' },
{ name: 'Colin', age: 40, country: 'AU' },
]"
:evenHighlights="false"
></Table>
import { Table } from '@/afcl'
const isoFlagToEmoji = (iso) => iso.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397))
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'BR' },
{ name: 'Colin', age: 40, country: 'AU' },
]"
>
<template #cell:country="{item}">
{{ isoFlagToEmoji(item.country) }}
</template>
</Table>
import { Table } from '@/afcl'
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'BR' },
{ name: 'Colin', age: 40, country: 'AU' },
]"
>
<template #header:country>
Geo
</template>
</Table>
Table provides front-end side pagination. You can set pageSize (default is 5) to set how many rows to show per page.
If there is less then pageSize rows, pagination will not be shown.
import { Table } from '@/afcl'
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'BR' },
{ name: 'Colin', age: 40, country: 'AU' },
]"
:pageSize="3"
>
</Table>
To load pages dynamically, simply pass async callback to data:
import { Table } from '@/afcl'
async function loadPageData(data) {
const { offset, limit } = data;
// in real app do await callAdminForthApi or await fetch to get date, use offset and limit value to slice data
return {
data: [
{ name: 'John', age: offset, country: 'US' },
{ name: 'Rick', age: offset+1, country: 'CA' },
{ name: 'Alice', age: offset+2, country: 'BR' },
],
total: 9 // should return total amount of records in database
}
}
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="loadPageData"
:pageSize="3">
</Table>
The page size is used as the limit for pagination.
Table supports column sorting out of the box.
sortable: true.Basic example (client-side sorting when data is an array):
import { Table } from '@/afcl'
<Table
:columns="[
{ label: 'Name', fieldName: 'name', sortable: true },
{ label: 'Age', fieldName: 'age', sortable: true },
// disable sort for a specific column
{ label: 'Country', fieldName: 'country', sortable: false },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'BR' },
{ name: 'Colin', age: 40, country: 'AU' },
]"
:pageSize="3"
/>
You can also predefine a default sort:
import { Table } from '@/afcl'
const rows = [
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'BR' },
];
<Table
:columns="[
{ label: 'Name', fieldName: 'name', sortable: true },
{ label: 'Age', fieldName: 'age', sortable: true },
{ label: 'Country', fieldName: 'country' },
]"
:data="rows"
defaultSortField="age"
defaultSortDirection="desc"
/>
Notes:
user.name.You can sort by nested properties of objects in rows using dot-notation in fieldName.
Example:
import { Table } from '@/afcl'
<Table
:columns="[
{ label: 'User Name', fieldName: 'user.name', sortable: true },
{ label: 'User Email', fieldName: 'user.email', sortable: true },
{ label: 'City', fieldName: 'user.address.city', sortable: true },
{ label: 'Country', fieldName: 'user.address.country' },
]"
:data="[
{ user: { name: 'Alice', email: 'alice@example.com', address: { city: 'Berlin', country: 'DE' } } },
{ user: { name: 'Bob', email: 'bob@example.com', address: { city: 'Paris', country: 'FR' } } },
{ user: { name: 'Carlos', email: 'carlos@example.com', address: { city: 'Madrid', country: 'ES' } } },
{ user: { name: 'Dana', email: 'dana@example.com', address: { city: 'Rome', country: 'IT' } } },
{ user: { name: 'Eve', email: 'eve@example.com', address: { city: null, country: 'US' } } },
]"
:pageSize="3"
/>
Behavior details:
user.address.city.null nested values always go to the bottom, for ascending and descending order alike.sortField (e.g. user.address.city) is passed so you can implement equivalent ordering on the backend.Edge cases to consider in your own data:
user.profile.settings.locale simply result in undefined and will follow the null ordering logic above.When you provide an async function to data, the table will pass the current sort along with pagination params.
Signature of the loader receives:
type LoaderArgs = {
offset: number;
limit: number;
sortField?: string; // undefined when unsorted
sortDirection?: 'asc' | 'desc'; // only when sortField is set
}
Example using fetch:
import { Table } from '@/afcl'
async function loadPageData({ offset, limit, sortField, sortDirection }) {
const url = new URL('/api/products', window.location.origin);
url.searchParams.set('offset', String(offset));
url.searchParams.set('limit', String(limit));
if (sortField) url.searchParams.set('sortField', sortField);
if (sortField && sortDirection) url.searchParams.set('sortDirection', sortDirection);
const { data, total } = await (await fetch(url)).json();
return { data, total };
}
<Table
:columns="[
{ label: 'ID', fieldName: 'id' },
{ label: 'Title', fieldName: 'title' },
{ label: 'Price', fieldName: 'price' },
]"
:data="loadPageData"
:pageSize="10"
/>
Events you can listen to:
<Table
:columns="columns"
:data="loadPageData"
@update:sortField="(f) => currentSortField = f"
@update:sortDirection="(d) => currentSortDirection = d"
@sort-change="({ field, direction }) => console.log('sort changed', field, direction)"
/>
For tables where you load data externally and pass them to data prop as array (including case with front-end pagination) you might want to show skeleton loaders in table externaly using isLoading props.
For tables with server-side paganation which use async function in data prop you can listen for @update:tableLoading to get table internal loading state, in other words this event will let you know when server side function started and finished execution, so you might use it e.g. to disable some external filter buttons and so on.
import { ref } from 'vue'
import { Table, Button } from '@/afcl'
const isTableLoading = ref(false);
const tableState = ref('loading');
async function loadPageData({ offset }) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // simulate network delay
return {
data: [
{ name: 'John', age: offset, country: 'US' },
{ name: 'Rick', age: offset + 1, country: 'CA' },
{ name: 'Alice', age: offset + 2, country: 'BR' },
],
total: 9,
};
}
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="loadPageData"
:isLoading="isTableLoading"
:pageSize="3"
@update:tableLoading="(loading) => tableState = loading ? 'loading' : 'loaded'"
>
</Table>
<p class="my-3">Table state: {{ tableState }}</p>
<Button @click="isTableLoading = !isTableLoading">Toggle loading state</Button>
If you want to make table header or pagination sticky, you can add makeHeaderSticky, makePaginationSticky
import { Table } from '@/afcl'
<Table
class="min-h-[200px] max-h-[250px]"
makeHeaderSticky
makePaginationSticky
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="[
{ name: 'John', age: 30, country: 'US' },
{ name: 'Rick', age: 25, country: 'CA' },
{ name: 'Alice', age: 35, country: 'UK' },
{ name: 'Colin', age: 40, country: 'AU' },
{ name: 'Dana', age: 28, country: 'IT' },
{ name: 'Eve', age: 33, country: 'DE' },
]"
></Table>
Sometimes you might want to allow user switch between pages, even if old request wasn't finished. For these porpuses you can use blockPaginationOnLoading and abortSignal in data callback:
import { Table } from '@/afcl'
async function loadPageData(data, abortSignal) {
const { offset, limit } = data;
// in real app do await callAdminForthApi or await fetch to get date, use offset and limit value to slice data
await new Promise(resolve => setTimeout(resolve, 500)) // simulate network delay
if (abortSignal.aborted) return; // since result won't be displayed, we stop computing
return {
data: [
{ name: 'John', age: offset, country: 'US' },
{ name: 'Rick', age: offset+1, country: 'CA' },
{ name: 'Alice', age: offset+2, country: 'BR' },
],
total: 30 // should return total amount of records in database
}
}
<Table
:columns="[
{ label: 'Name', fieldName: 'name' },
{ label: 'Age', fieldName: 'age' },
{ label: 'Country', fieldName: 'country' },
]"
:data="loadPageData"
:blockPaginationOnLoading="false"
:pageSize="3">
</Table>
import { JsonViewer } from '@/afcl'
<JsonViewer
:value="[
{
id: 1,
name: 'Alice',
meta: {
age: 30,
hobbies: ['reading', 'biking'],
}
},
{
id: 2,
name: 'Bob',
}
]"
:expandDepth="2"
/>
The Country Flag component displays national flags using ISO 3166-1 alpha-2 country codes. A flag has a hairline edge, so white stripes stay visible on a white page. Only the SVG of a shown flag is downloaded.
import { CountryFlag } from '@/afcl'
<CountryFlag countryCode="ua" />
size is md (default, 3224) or sm (1612) for menus, selects and lists:
import { CountryFlag } from '@/afcl'
<div class="flex items-center gap-4">
<span class="flex items-center gap-2 text-sm"><CountryFlag countryCode="jp" size="sm" /></span>
<CountryFlag countryCode="jp" />
</div>
import { ProgressBar } from '@/afcl'
<ProgressBar
:currentValue="2600"
:minValue="0"
:maxValue="5000"
/>
Custom labels in the ProgressBar component allow you to customize the text displayed on the left and right sides of the progress bar. You can also customize the format of the value and the progress text.
import { ProgressBar } from '@/afcl'
<ProgressBar
:currentValue="1070"
:minValue="0"
:maxValue="5000"
:leftLabel="'Level 2'"
:rightLabel="'Level 3'"
:formatter="(value: number) => `${value} points`"
:progressFormatter="(value: number, percentage: number) => `${value} done`"
/>
import { ProgressBar } from '@/afcl'
<ProgressBar
:currentValue="2600"
:minValue="0"
:maxValue="5000"
showAnimation
/>
import { ProgressBar } from '@/afcl'
<ProgressBar
:currentValue="2600"
:minValue="0"
:maxValue="5000"
:height="6"
/>
Skeleton component is used to display a loading state for a component. You can use prop type to set the type of the skeleton.
import { Skeleton } from '@/afcl'
<div class="flex flex-col gap-2">
<Skeleton class="w-full h-4" />
<Skeleton class="w-full h-2" />
<Skeleton class="w-full h-2" />
<Skeleton class="w-full h-2" />
<Skeleton class="w-full h-2" />
</div>
import { Skeleton } from '@/afcl'
<Skeleton type="image" class="w-full h-40" />
import { Skeleton } from '@/afcl'
<div class="flex flex-col gap-2">
<Skeleton type="input" class="w-full h-4" />
<Skeleton type="input" class="w-full h-2" />
<Skeleton type="input" class="w-full h-2" />
<Skeleton type="input" class="w-full h-2" />
<Skeleton type="input" class="w-full h-2" />
</div>
import { Skeleton } from '@/afcl'
<Skeleton type="video" class="w-full h-40" />
import { Skeleton } from '@/afcl'
<Skeleton type="avatar" class="w-20 h-20" />
Spinner component is used to display a loading state for a component.
import { Spinner } from '@/afcl'
<Spinner class="w-10 h-10" />
| Web Proxy Viewer | New URL | Original Page |