| [ Web Proxy ] |
| Viewing: https://adminforth.dev/docs/tutorial/Customization/afcl/dataDisplay/#json-viewer | [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>
The Json Viewer lays a JSON value out like Prettier: a node which fits in a line (up to 60 characters, fewer in a narrow place) stays on one line, a longer one breaks by keys. A chevron on the left folds a node, a folded node shows its value in one line and its size. Nodes deeper than expandDepth (default 1) start folded. The copy button appears on hover.
import { JsonViewer } from '@/afcl'
<JsonViewer
:value="[
{
id: 1,
name: 'Alice',
meta: {
age: 30,
hobbies: ['reading', 'biking'],
}
},
{
id: 2,
name: 'Bob',
}
]"
:expandDepth="2"
/>
inline shows the value in one line for table cells, a click opens the whole value in a popover. The list page of AdminForth shows JSON columns this way.
import { JsonViewer } from '@/afcl'
<JsonViewer
inline
:value="{ campaign: 'cs2026', channel: 'linkedin', touches: 3, verified: true }"
/>
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>
A Badge labels a status or a category. Variants are default, success, warning, danger, accent (the primary color of the theme) and muted.
import { Badge } from '@/afcl'
<div class="flex flex-wrap items-center gap-2">
<Badge>Default</Badge>
<Badge variant="success">Paid</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="danger">Failed</Badge>
<Badge variant="accent">New</Badge>
<Badge variant="muted">Archived</Badge>
</div>
To render values of an enum column as badges in the list or show view, use the Enum Badge renderer.
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 |