Vitral 0.2
Dados

DataGrid

Uma tabela de dados que ordena, filtra, pagina e seleciona, localmente ou no seu servidor, com colunas que o leitor pode redimensionar, reordenar, fixar e ocultar.

Importação

main.ts
import { DataGrid } from '@vitral/vue';

Ordenar, filtrar, paginar e selecionar

Clique num cabeçalho para ordenar, digite abaixo dele para filtrar (os acentos são ignorados: “sao” encontra São Paulo), busque em todas as colunas de uma vez pelo cabeçalho, e marque linhas.

People0 selected
People
Ximena Diallo
São Paulo · Brazil
15 de set. de 2020
€ 4.586,25
Offline
Noémie Nowak
Zürich · Switzerland
7 de out. de 2023
€ 5.484,63
Offline
Bruno Nowak
Kraków · Poland
23 de dez. de 2022
€ 456,73
Away
Quentin Gómez
Tōkyō · Japan
26 de nov. de 2023
€ 5.444,92
Active
Hanna Mendes
Reykjavík · Iceland
9 de jun. de 2021
€ 16.745,23
Away
Wiktor Lima
Kraków · Poland
15 de jul. de 2020
€ 6.428,86
Active
Bruno Rocha
Tōkyō · Japan
26 de ago. de 2021
€ 8.297,10
Active
Gustavo Rocha
Québec · Canada
15 de abr. de 2021
€ 10.280,87
Active
<script setup lang="ts">import { Column, DataGrid, Icon, InputText, Select, useVitral } from '@vitral/vue';import { computed, ref } from 'vue'; type Status = 'Active' | 'Away' | 'Offline' | 'Blocked'; interface Person {    id: number;    name: string;    city: string;    country: string;    joined: Date;    balance: number;    status: Status;} // A small deterministic generator, so the demo shows the same people every time.let seed = 7;const random = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;const pick = <T,>(list: readonly T[]) => list[Math.floor(random() * list.length)]!; const firstNames = ['Ana', 'Bruno', 'Carla', 'Diego', 'Élise', 'Fatou', 'Gustavo', 'Hanna', 'Iñaki', 'João', 'Kenji', 'Lucía', 'Mateus', 'Noémie', 'Oskar', 'Paula', 'Quentin', 'Raquel', 'Søren', 'Tomás', 'Valéria', 'Wiktor', 'Ximena', 'Zoë'];const lastNames = ['Souza', 'Lima', 'Mendes', 'Ferreira', 'Martin', 'Diallo', 'Rocha', 'Müller', 'Etxeberria', 'Pereira', 'Sato', 'Gómez', 'Nowak', 'Dubois', 'Jónsson', 'Costa', 'Fontaine', 'Álvarez'];const places: [string, string][] = [    ['São Paulo', 'Brazil'],    ['Recife', 'Brazil'],    ['Florianópolis', 'Brazil'],    ['Lisboa', 'Portugal'],    ['Évora', 'Portugal'],    ['Montréal', 'Canada'],    ['Québec', 'Canada'],    ['München', 'Germany'],    ['Köln', 'Germany'],    ['Córdoba', 'Argentina'],    ['Bogotá', 'Colombia'],    ['Kraków', 'Poland'],    ['Zürich', 'Switzerland'],    ['Reykjavík', 'Iceland'],    ['Tōkyō', 'Japan'],    ['Málaga', 'Spain']];const statuses: Status[] = ['Active', 'Active', 'Active', 'Away', 'Offline', 'Blocked']; const people: Person[] = Array.from({ length: 64 }, (_, i) => {    const [city, country] = pick(places);    return {        id: i + 1,        name: `${pick(firstNames)} ${pick(lastNames)}`,        city,        country,        joined: new Date(2019 + Math.floor(random() * 7), Math.floor(random() * 12), 1 + Math.floor(random() * 28)),        balance: Math.round(random() * 2_000_000) / 100 - 2000,        status: pick(statuses)    };}); const { config } = useVitral();const money = computed(() => new Intl.NumberFormat(config.locale.code, { style: 'currency', currency: 'EUR' }));const day = computed(() => new Intl.DateTimeFormat(config.locale.code, { dateStyle: 'medium' }));const statusOptions: Status[] = ['Active', 'Away', 'Offline', 'Blocked']; const filters = ref({    global: { value: null as string | null, matchMode: 'contains' },    name: { value: null as string | null, matchMode: 'contains' },    city: { value: null as string | null, matchMode: 'startsWith' },    status: { value: null as Status | null, matchMode: 'equals' }});const selected = ref<Person[]>([]);</script> <template>    <DataGrid        v-model:filters="filters"        v-model:selection="selected"        :value="people"        data-key="id"        caption="People"        filter-display="row"        :global-filter-fields="['name', 'city', 'country']"        paginator        :rows="8"        :rows-per-page-options="[8, 16, 32]"        paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"        removable-sort        striped-rows        class="demo-table"    >        <template #header>            <div class="demo-table-bar">                <strong>People</strong>                <small style="color: var(--vt-text-muted-color)">{{ selected.length }} selected</small>                <InputText v-model="filters.global.value" placeholder="Search people" aria-label="Search people" size="small" clearable class="demo-table-search">                    <template #prefix><Icon icon="search" /></template>                </InputText>            </div>        </template>        <Column selection-mode="multiple" />        <Column field="name" header="Name" sortable filter-placeholder="Name" />        <Column field="city" header="City" sortable filter-placeholder="Starts with">            <template #body="{ data }">                {{ data.city }} <span class="demo-muted">· {{ data.country }}</span>            </template>        </Column>        <Column field="joined" header="Joined" sortable>            <template #body="{ data }">{{ day.format(data.joined) }}</template>        </Column>        <Column field="balance" header="Balance" sortable align="right">            <template #body="{ data }">                <span :class="{ 'demo-negative': data.balance < 0 }">{{ money.format(data.balance) }}</span>            </template>        </Column>        <Column field="status" header="Status" sortable>            <template #body="{ data }">                <span class="demo-status"><span :class="['demo-dot', `demo-dot-${data.status.toLowerCase()}`]" aria-hidden="true" />{{ data.status }}</span>            </template>            <template #filter="{ filterModel }">                <Select v-if="filterModel && 'value' in filterModel" v-model="filterModel.value" :options="statusOptions" placeholder="Any" show-clear size="small" aria-label="Filter Status" fluid />            </template>        </Column>    </DataGrid></template> <style scoped>.demo-table {    width: 100%;} .demo-table-bar {    display: flex;    align-items: center;    gap: 0.75rem;    flex-wrap: wrap;} .demo-table-search {    margin-inline-start: auto;    width: 16rem;} .demo-muted {    color: var(--vt-text-muted-color);} .demo-negative {    color: var(--vt-danger-color);} .demo-status {    display: inline-flex;    align-items: center;    gap: 0.5rem;} .demo-dot {    width: 0.5rem;    height: 0.5rem;    border-radius: 999px;    background: var(--vt-text-muted-color);} .demo-dot-active {    background: var(--vt-success-color);} .demo-dot-away {    background: var(--vt-warn-color);} .demo-dot-blocked {    background: var(--vt-danger-color);}</style>

Ordenação múltipla, seleção única, linhas de grade

Ordenado por país e depois por saldo, do maior para o menor. Ctrl-clique (⌘-clique) num cabeçalho para adicioná-lo à ordenação. Clique numa linha, ou foque as linhas e use as setas e Espaço, para escolher uma. O cabeçalho continua visível enquanto o corpo rola.

Diego FerreiraArgentinaCórdoba
€ 15.492,29
Noémie FontaineArgentinaCórdoba
€ 9.472,17
Ximena FerreiraArgentinaCórdoba
€ 2.865,18
Bruno MartinBrazilFlorianópolis
€ 17.876,33
Élise DuboisBrazilSão Paulo
€ 14.269,79
Bruno SouzaBrazilFlorianópolis
€ 13.363,03
Bruno LimaBrazilSão Paulo
€ 12.562,40
Ana GómezBrazilRecife
€ 11.750,51
Fatou FerreiraBrazilSão Paulo
€ 10.543,41
Oskar MartinBrazilSão Paulo
€ 10.400,34
Ximena DialloBrazilSão Paulo
€ 4.586,25
Bruno RochaCanadaMontréal
€ 17.103,69
João EtxeberriaCanadaMontréal
€ 11.302,35
Wiktor JónssonCanadaQuébec
€ 10.857,06
Gustavo RochaCanadaQuébec
€ 10.280,87
Élise SatoCanadaQuébec
€ 4.759,90
Hanna SouzaCanadaQuébec
€ 2.684,86
Ximena SouzaColombiaBogotá
€ 9.305,62
Valéria FontaineColombiaBogotá
€ 7.948,90
Ana JónssonColombiaBogotá
€ 4.625,47
Ana LimaGermanyMünchen
€ 15.860,46
Kenji CostaGermanyKöln
€ 11.732,89
Hanna MartinGermanyMünchen
€ 7.969,50
Mateus DuboisGermanyKöln
€ 3.976,73
João EtxeberriaGermanyKöln
€ 3.947,82
Carla LimaGermanyMünchen
€ 2.002,67
Hanna MendesIcelandReykjavík
€ 16.745,23
Diego CostaIcelandReykjavík
€ 13.914,59
Fatou FontaineIcelandReykjavík
€ 13.839,42
Wiktor FontaineIcelandReykjavík
€ 13.239,60
Tomás DuboisIcelandReykjavík
€ 12.242,07
Tomás FerreiraIcelandReykjavík
€ 8.764,27
Bruno RochaJapanTōkyō
€ 8.297,10
Kenji CostaJapanTōkyō
€ 8.098,99
Carla DialloJapanTōkyō
€ 5.945,52
Quentin GómezJapanTōkyō
€ 5.444,92
João MartinJapanTōkyō
€ 5.028,76
Valéria ÁlvarezJapanTōkyō
€ 2.964,27
Wiktor CostaJapanTōkyō
-€ 1.790,27
Fatou RochaJapanTōkyō
-€ 1.940,60
Iñaki DuboisPolandKraków
€ 17.709,64
Wiktor CostaPolandKraków
€ 17.144,37
João GómezPolandKraków
€ 12.230,50
Wiktor LimaPolandKraków
€ 6.428,86
Wiktor FerreiraPolandKraków
€ 1.503,44
Wiktor CostaPolandKraków
€ 504,85
Bruno NowakPolandKraków
€ 456,73
Wiktor SouzaPolandKraków
-€ 303,08
Fatou FerreiraPortugalÉvora
€ 12.014,06
Fatou JónssonPortugalLisboa
€ 10.703,89
Ana EtxeberriaPortugalLisboa
€ 7.618,88
Raquel DialloPortugalLisboa
-€ 493,76
Bruno LimaSpainMálaga
€ 17.131,64
Noémie SatoSpainMálaga
€ 9.951,09
João NowakSpainMálaga
€ 4.612,00
Fatou GómezSpainMálaga
€ 755,90
Kenji NowakSpainMálaga
-€ 776,43
Carla FontaineSwitzerlandZürich
€ 15.325,86
Ximena EtxeberriaSwitzerlandZürich
€ 11.252,24
Søren MüllerSwitzerlandZürich
€ 11.236,83
Élise LimaSwitzerlandZürich
€ 8.895,92
Noémie NowakSwitzerlandZürich
€ 5.484,63
Zoë MendesSwitzerlandZürich
€ 2.202,54
Wiktor LimaSwitzerlandZürich
€ 1.071,43
<script setup lang="ts">import { Column, DataGrid, useVitral } from '@vitral/vue';import { computed, ref } from 'vue'; type Status = 'Active' | 'Away' | 'Offline' | 'Blocked'; interface Person {    id: number;    name: string;    city: string;    country: string;    joined: Date;    balance: number;    status: Status;} // A small deterministic generator, so the demo shows the same people every time.let seed = 7;const random = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;const pick = <T,>(list: readonly T[]) => list[Math.floor(random() * list.length)]!; const firstNames = ['Ana', 'Bruno', 'Carla', 'Diego', 'Élise', 'Fatou', 'Gustavo', 'Hanna', 'Iñaki', 'João', 'Kenji', 'Lucía', 'Mateus', 'Noémie', 'Oskar', 'Paula', 'Quentin', 'Raquel', 'Søren', 'Tomás', 'Valéria', 'Wiktor', 'Ximena', 'Zoë'];const lastNames = ['Souza', 'Lima', 'Mendes', 'Ferreira', 'Martin', 'Diallo', 'Rocha', 'Müller', 'Etxeberria', 'Pereira', 'Sato', 'Gómez', 'Nowak', 'Dubois', 'Jónsson', 'Costa', 'Fontaine', 'Álvarez'];const places: [string, string][] = [    ['São Paulo', 'Brazil'],    ['Recife', 'Brazil'],    ['Florianópolis', 'Brazil'],    ['Lisboa', 'Portugal'],    ['Évora', 'Portugal'],    ['Montréal', 'Canada'],    ['Québec', 'Canada'],    ['München', 'Germany'],    ['Köln', 'Germany'],    ['Córdoba', 'Argentina'],    ['Bogotá', 'Colombia'],    ['Kraków', 'Poland'],    ['Zürich', 'Switzerland'],    ['Reykjavík', 'Iceland'],    ['Tōkyō', 'Japan'],    ['Málaga', 'Spain']];const statuses: Status[] = ['Active', 'Active', 'Active', 'Away', 'Offline', 'Blocked']; const people: Person[] = Array.from({ length: 64 }, (_, i) => {    const [city, country] = pick(places);    return {        id: i + 1,        name: `${pick(firstNames)} ${pick(lastNames)}`,        city,        country,        joined: new Date(2019 + Math.floor(random() * 7), Math.floor(random() * 12), 1 + Math.floor(random() * 28)),        balance: Math.round(random() * 2_000_000) / 100 - 2000,        status: pick(statuses)    };}); const { config } = useVitral();const money = computed(() => new Intl.NumberFormat(config.locale.code, { style: 'currency', currency: 'EUR' })); const multiSortMeta = ref([    { field: 'country', order: 1 as const },    { field: 'balance', order: -1 as const }]);const chosen = ref<Person | null>(null);</script> <template>    <DataGrid        v-model:multi-sort-meta="multiSortMeta"        v-model:selection="chosen"        :value="people"        data-key="id"        aria-label="People by country"        sort-mode="multiple"        selection-mode="single"        size="small"        show-gridlines        scrollable        scroll-height="320px"        class="demo-table"    >        <Column field="name" header="Name" sortable />        <Column field="country" header="Country" sortable />        <Column field="city" header="City" sortable />        <Column field="balance" header="Balance" sortable align="right">            <template #body="{ data }">{{ money.format(data.balance) }}</template>        </Column>        <template #footer>            <span>{{ chosen ? `${chosen.name}, ${chosen.city}` : 'No one chosen' }}</span>        </template>    </DataGrid></template> <style scoped>.demo-table {    width: 100%;}</style>

Colunas que o leitor organiza

Arraste um cabeçalho para mover uma coluna, ou segure Ctrl (⌘) e pressione uma seta. Arraste a borda de um cabeçalho para definir a largura — a alça está na ordem de tabulação, então as setas também fazem isso, e Shift dá passos maiores. Name e Balance ficam fixas nas bordas e permanecem enquanto o resto rola para o lado. O botão abre a lista de colunas. Tudo o que o leitor faz é um único objeto, `columnLayout`, que uma aplicação pode guardar e devolver.

RoleStatusBalance
Ximena DialloBrazilSão PauloOffline
€ 4.586,25
Noémie NowakSwitzerlandZürichOffline
€ 5.484,63
Bruno NowakPolandKrakówAway
€ 456,73
Quentin GómezJapanTōkyōActive
€ 5.444,92
Hanna MendesIcelandReykjavíkAway
€ 16.745,23
Wiktor LimaPolandKrakówActive
€ 6.428,86
Bruno RochaJapanTōkyōActive
€ 8.297,10
Gustavo RochaCanadaQuébecActive
€ 10.280,87
Nothing changed yet.
<script setup lang="ts">import { Button, Column, DataGrid, StackPanel, useVitral, type ColumnLayoutLike } from '@vitral/vue';import { computed, ref } from 'vue'; const people = [    { id: 1, name: 'Ximena Diallo', city: 'São Paulo', country: 'Brazil', balance: 4586.25, status: 'Offline' },    { id: 2, name: 'Noémie Nowak', city: 'Zürich', country: 'Switzerland', balance: 5484.63, status: 'Offline' },    { id: 3, name: 'Bruno Nowak', city: 'Kraków', country: 'Poland', balance: 456.73, status: 'Away' },    { id: 4, name: 'Quentin Gómez', city: 'Tōkyō', country: 'Japan', balance: 5444.92, status: 'Active' },    { id: 5, name: 'Hanna Mendes', city: 'Reykjavík', country: 'Iceland', balance: 16745.23, status: 'Away' },    { id: 6, name: 'Wiktor Lima', city: 'Kraków', country: 'Poland', balance: 6428.86, status: 'Active' },    { id: 7, name: 'Bruno Rocha', city: 'Tōkyō', country: 'Japan', balance: 8297.1, status: 'Active' },    { id: 8, name: 'Gustavo Rocha', city: 'Québec', country: 'Canada', balance: 10280.87, status: 'Active' }]; const { config } = useVitral();const money = computed(() => new Intl.NumberFormat(config.locale.code, { style: 'currency', currency: 'EUR' })); // What the reader has done to the columns: an application would store this.const layout = ref<ColumnLayoutLike | null>(null);const layoutSummary = computed(() => {    const it = layout.value;    if (!it) return 'Nothing changed yet.';    const parts = [        it.order?.length ? `order: ${it.order.join(', ')}` : '',        it.hidden?.length ? `hidden: ${it.hidden.join(', ')}` : '',        Object.keys(it.widths ?? {}).length ? `${Object.keys(it.widths!).length} widths` : ''    ].filter(Boolean);    return parts.length ? parts.join(' · ') : 'Nothing changed yet.';});</script> <template>    <StackPanel spacing="1rem" style="width: 100%">        <DataGrid            v-model:column-layout="layout"            :value="people"            data-key="id"            aria-label="People, arranged"            resizable-columns            reorderable-columns            column-toggle            scrollable            size="small"            show-gridlines            class="demo-table"            style="max-width: 100%"        >            <Column field="name" header="Name" sortable pinned="left" :width="180" />            <Column field="country" header="Country" sortable :width="140" />            <Column field="city" header="City" sortable :width="160" />            <Column field="role" header="Role" :width="150" />            <Column field="status" header="Status" :width="120" />            <Column field="balance" header="Balance" align="right" pinned="right" :width="130">                <template #body="{ data }">{{ money.format(data.balance) }}</template>            </Column>        </DataGrid>        <div class="demo-table-bar">            <Button size="small" severity="secondary" variant="outlined" @click="layout = null">Reset the layout</Button>            <small style="color: var(--vt-text-muted-color)">{{ layoutSummary }}</small>        </div>    </StackPanel></template> <style scoped>.demo-table {    width: 100%;} .demo-table-bar {    display: flex;    align-items: center;    gap: 0.75rem;    flex-wrap: wrap;}</style>

Duas tabelas, um só conjunto de colunas

`group` é um nome que duas tabelas compartilham: reorganize uma coluna em qualquer uma e as duas acompanham, e rolar uma para o lado rola a outra. As linhas são de cada tabela — aqui, este trimestre acima do trimestre passado.

NameCountryCityBalance
Ximena DialloBrazilSão Paulo
€ 4.586,25
Noémie NowakSwitzerlandZürich
€ 5.484,63
Bruno NowakPolandKraków
€ 456,73
Quentin GómezJapanTōkyō
€ 5.444,92
NameCountryCityBalance
Hanna MendesIcelandReykjavík
€ 16.745,23
Wiktor LimaPolandKraków
€ 6.428,86
Bruno RochaJapanTōkyō
€ 8.297,10
Gustavo RochaCanadaQuébec
€ 10.280,87
<script setup lang="ts">import { Column, DataGrid, StackPanel, useVitral } from '@vitral/vue';import { computed } from 'vue'; const thisQuarter = [    { id: 1, name: 'Ximena Diallo', city: 'São Paulo', country: 'Brazil', balance: 4586.25 },    { id: 2, name: 'Noémie Nowak', city: 'Zürich', country: 'Switzerland', balance: 5484.63 },    { id: 3, name: 'Bruno Nowak', city: 'Kraków', country: 'Poland', balance: 456.73 },    { id: 4, name: 'Quentin Gómez', city: 'Tōkyō', country: 'Japan', balance: 5444.92 }];const lastQuarter = [    { id: 5, name: 'Hanna Mendes', city: 'Reykjavík', country: 'Iceland', balance: 16745.23 },    { id: 6, name: 'Wiktor Lima', city: 'Kraków', country: 'Poland', balance: 6428.86 },    { id: 7, name: 'Bruno Rocha', city: 'Tōkyō', country: 'Japan', balance: 8297.1 },    { id: 8, name: 'Gustavo Rocha', city: 'Québec', country: 'Canada', balance: 10280.87 }]; const { config } = useVitral();const money = computed(() => new Intl.NumberFormat(config.locale.code, { style: 'currency', currency: 'EUR' }));</script> <template>    <StackPanel spacing="0.5rem" style="width: 100%">        <DataGrid :value="thisQuarter" data-key="id" aria-label="This quarter" group="quarters" resizable-columns reorderable-columns size="small" scrollable class="demo-table">            <Column field="name" header="Name" :width="170" />            <Column field="country" header="Country" :width="140" />            <Column field="city" header="City" :width="150" />            <Column field="balance" header="Balance" align="right" :width="130">                <template #body="{ data }">{{ money.format(data.balance) }}</template>            </Column>        </DataGrid>        <DataGrid :value="lastQuarter" data-key="id" aria-label="Last quarter" group="quarters" resizable-columns reorderable-columns size="small" scrollable class="demo-table">            <Column field="name" header="Name" :width="170" />            <Column field="country" header="Country" :width="140" />            <Column field="city" header="City" :width="150" />            <Column field="balance" header="Balance" align="right" :width="130">                <template #body="{ data }">{{ money.format(data.balance) }}</template>            </Column>        </DataGrid>    </StackPanel></template> <style scoped>.demo-table {    width: 100%;}</style>

Agrupado, do jeito que se lê uma base

`group-by` reúne as linhas por um campo e põe um título sobre cada bloco, com a contagem e um botão para recolher. Isso acontece depois da consulta, então a ordenação e os filtros continuam decidindo quais linhas existem — e as linhas são reunidas por valor, e não por vizinhança, então ordenar por outra coluna mantém cada grupo junto e ordena dentro dele. `v-model:collapsedGroups` é o que o leitor fechou, e uma aplicação pode guardar isso.

0 shut
People by group
Ana EtxeberriaLisboaPortugal
Active
€ 7.618,88
Fatou FerreiraÉvoraPortugal
Offline
€ 12.014,06
Fatou JónssonLisboaPortugal
Offline
€ 10.703,89
Raquel DialloLisboaPortugal
Offline
-€ 493,76
Ana GómezRecifeBrazil
Active
€ 11.750,51
Bruno LimaSão PauloBrazil
Away
€ 12.562,40
Bruno MartinFlorianópolisBrazil
Active
€ 17.876,33
Bruno SouzaFlorianópolisBrazil
Active
€ 13.363,03
Élise DuboisSão PauloBrazil
Active
€ 14.269,79
Fatou FerreiraSão PauloBrazil
Blocked
€ 10.543,41
Oskar MartinSão PauloBrazil
Away
€ 10.400,34
Ximena DialloSão PauloBrazil
Offline
€ 4.586,25
Ana JónssonBogotáColombia
Offline
€ 4.625,47
Valéria FontaineBogotáColombia
Away
€ 7.948,90
Ximena SouzaBogotáColombia
Offline
€ 9.305,62
Ana LimaMünchenGermany
Active
€ 15.860,46
Carla LimaMünchenGermany
Away
€ 2.002,67
Hanna MartinMünchenGermany
Blocked
€ 7.969,50
João EtxeberriaKölnGermany
Active
€ 3.947,82
Kenji CostaKölnGermany
Active
€ 11.732,89
Mateus DuboisKölnGermany
Away
€ 3.976,73
Bruno LimaMálagaSpain
Active
€ 17.131,64
Fatou GómezMálagaSpain
Blocked
€ 755,90
João NowakMálagaSpain
Active
€ 4.612,00
Kenji NowakMálagaSpain
Active
-€ 776,43
Noémie SatoMálagaSpain
Active
€ 9.951,09
Bruno NowakKrakówPoland
Away
€ 456,73
Iñaki DuboisKrakówPoland
Blocked
€ 17.709,64
João GómezKrakówPoland
Away
€ 12.230,50
Wiktor CostaKrakówPoland
Blocked
€ 17.144,37
Wiktor CostaKrakówPoland
Blocked
€ 504,85
Wiktor FerreiraKrakówPoland
Active
€ 1.503,44
Wiktor LimaKrakówPoland
Active
€ 6.428,86
Wiktor SouzaKrakówPoland
Active
-€ 303,08
Bruno RochaTōkyōJapan
Active
€ 8.297,10
Carla DialloTōkyōJapan
Active
€ 5.945,52
Fatou RochaTōkyōJapan
Blocked
-€ 1.940,60
João MartinTōkyōJapan
Away
€ 5.028,76
Kenji CostaTōkyōJapan
Offline
€ 8.098,99
Quentin GómezTōkyōJapan
Active
€ 5.444,92
Valéria ÁlvarezTōkyōJapan
Offline
€ 2.964,27
Wiktor CostaTōkyōJapan
Active
-€ 1.790,27
Bruno RochaMontréalCanada
Offline
€ 17.103,69
Élise SatoQuébecCanada
Offline
€ 4.759,90
Gustavo RochaQuébecCanada
Active
€ 10.280,87
Hanna SouzaQuébecCanada
Blocked
€ 2.684,86
João EtxeberriaMontréalCanada
Away
€ 11.302,35
Wiktor JónssonQuébecCanada
Active
€ 10.857,06
Carla FontaineZürichSwitzerland
Offline
€ 15.325,86
Élise LimaZürichSwitzerland
Active
€ 8.895,92
Noémie NowakZürichSwitzerland
Offline
€ 5.484,63
Søren MüllerZürichSwitzerland
Away
€ 11.236,83
Wiktor LimaZürichSwitzerland
Active
€ 1.071,43
Ximena EtxeberriaZürichSwitzerland
Away
€ 11.252,24
Zoë MendesZürichSwitzerland
Away
€ 2.202,54
Diego CostaReykjavíkIceland
Offline
€ 13.914,59
Fatou FontaineReykjavíkIceland
Away
€ 13.839,42
Hanna MendesReykjavíkIceland
Away
€ 16.745,23
Tomás DuboisReykjavíkIceland
Active
€ 12.242,07
Tomás FerreiraReykjavíkIceland
Offline
€ 8.764,27
Wiktor FontaineReykjavíkIceland
Away
€ 13.239,60
Diego FerreiraCórdobaArgentina
Away
€ 15.492,29
Noémie FontaineCórdobaArgentina
Away
€ 9.472,17
Ximena FerreiraCórdobaArgentina
Active
€ 2.865,18
<script setup lang="ts">import { Button, Column, DataGrid, Icon, InputText, SelectButton, useVitral } from '@vitral/vue';import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; type Status = 'Active' | 'Away' | 'Offline' | 'Blocked'; interface Person {    id: number;    name: string;    city: string;    country: string;    joined: Date;    balance: number;    status: Status;} // A small deterministic generator, so the demo shows the same people every time.let seed = 7;const random = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;const pick = <T,>(list: readonly T[]) => list[Math.floor(random() * list.length)]!; const firstNames = ['Ana', 'Bruno', 'Carla', 'Diego', 'Élise', 'Fatou', 'Gustavo', 'Hanna', 'Iñaki', 'João', 'Kenji', 'Lucía', 'Mateus', 'Noémie', 'Oskar', 'Paula', 'Quentin', 'Raquel', 'Søren', 'Tomás', 'Valéria', 'Wiktor', 'Ximena', 'Zoë'];const lastNames = ['Souza', 'Lima', 'Mendes', 'Ferreira', 'Martin', 'Diallo', 'Rocha', 'Müller', 'Etxeberria', 'Pereira', 'Sato', 'Gómez', 'Nowak', 'Dubois', 'Jónsson', 'Costa', 'Fontaine', 'Álvarez'];const places: [string, string][] = [    ['São Paulo', 'Brazil'],    ['Recife', 'Brazil'],    ['Florianópolis', 'Brazil'],    ['Lisboa', 'Portugal'],    ['Évora', 'Portugal'],    ['Montréal', 'Canada'],    ['Québec', 'Canada'],    ['München', 'Germany'],    ['Köln', 'Germany'],    ['Córdoba', 'Argentina'],    ['Bogotá', 'Colombia'],    ['Kraków', 'Poland'],    ['Zürich', 'Switzerland'],    ['Reykjavík', 'Iceland'],    ['Tōkyō', 'Japan'],    ['Málaga', 'Spain']];const statuses: Status[] = ['Active', 'Active', 'Active', 'Away', 'Offline', 'Blocked']; const people: Person[] = Array.from({ length: 64 }, (_, i) => {    const [city, country] = pick(places);    return {        id: i + 1,        name: `${pick(firstNames)} ${pick(lastNames)}`,        city,        country,        joined: new Date(2019 + Math.floor(random() * 7), Math.floor(random() * 12), 1 + Math.floor(random() * 28)),        balance: Math.round(random() * 2_000_000) / 100 - 2000,        status: pick(statuses)    };}); const { config } = useVitral();const money = computed(() => new Intl.NumberFormat(config.locale.code, { style: 'currency', currency: 'EUR' })); const groupField = ref<'country' | 'status'>('country');const collapsed = ref<string[]>([]);const boardFilters = ref({ global: { value: null as string | null, matchMode: 'contains' } });const board = ref<HTMLElement | null>(null);const wide = ref(false); /** The browser's own full screen, so the grid gets the whole display and nothing else. */async function toggleWide() {    if (!document.fullscreenElement) await board.value?.requestFullscreen?.().catch(() => {});    else await document.exitFullscreen().catch(() => {});}const onFullscreen = () => (wide.value = !!document.fullscreenElement);onMounted(() => document.addEventListener('fullscreenchange', onFullscreen));onBeforeUnmount(() => document.removeEventListener('fullscreenchange', onFullscreen));</script> <template>    <div ref="board" style="display: flex; flex-direction: column; gap: 1rem; width: 100%; background: var(--vt-content-background)">        <div class="demo-table-bar">            <SelectButton v-model="groupField" :options="[{ label: 'By country', value: 'country' }, { label: 'By status', value: 'status' }]" option-label="label" option-value="value" size="small" aria-label="Group by" />            <InputText v-model="boardFilters.global.value" placeholder="Search" aria-label="Search rows" size="small" clearable class="demo-table-search">                <template #prefix><Icon icon="search" /></template>            </InputText>            <small style="color: var(--vt-text-muted-color)">{{ collapsed.length }} shut</small>            <Button :label="wide ? 'Close' : 'Full screen'" :icon="wide ? 'restore' : 'maximize'" severity="secondary" variant="outlined" size="small" @click="toggleWide" />        </div>        <DataGrid            v-model:filters="boardFilters"            v-model:collapsed-groups="collapsed"            :value="people"            :group-by="groupField"            :group-label="({ value, count }) => `${value} · ${count}`"            data-key="id"            caption="People by group"            :global-filter-fields="['name', 'city', 'country']"            sort-field="name"            :sort-order="1"            removable-sort            show-gridlines            scrollable            :scroll-height="wide ? 'calc(100vh - 8rem)' : '22rem'"            class="demo-table"        >            <Column field="name" header="Name" sortable />            <Column field="city" header="City" sortable />            <Column field="country" header="Country" sortable />            <Column field="status" header="Status" sortable>                <template #body="{ data }">                    <span class="demo-status"><span :class="['demo-dot', `demo-dot-${data.status.toLowerCase()}`]" aria-hidden="true" />{{ data.status }}</span>                </template>            </Column>            <Column field="balance" header="Balance" sortable align="right">                <template #body="{ data }">{{ money.format(data.balance) }}</template>            </Column>        </DataGrid>    </div></template> <style scoped>.demo-table {    width: 100%;} .demo-table-bar {    display: flex;    align-items: center;    gap: 0.75rem;    flex-wrap: wrap;} .demo-table-search {    margin-inline-start: auto;    width: 16rem;} .demo-status {    display: inline-flex;    align-items: center;    gap: 0.5rem;} .demo-dot {    width: 0.5rem;    height: 0.5rem;    border-radius: 999px;    background: var(--vt-text-muted-color);} .demo-dot-active {    background: var(--vt-success-color);} .demo-dot-away {    background: var(--vt-warn-color);} .demo-dot-blocked {    background: var(--vt-danger-color);}</style>

De uma fonte de dados

As linhas vêm de `createDataSource({ load })`, que faz as vezes de um servidor que responde em 450 ms. A tabela pede uma página por vez, com a ordenação e a busca; uma resposta lenta que chega depois de uma mais nova é descartada.

Server1 requests
Nenhuma opção disponível
Carregando…
<script setup lang="ts">import { queryData } from '@vitral/core';import { Column, createDataSource, DataGrid, Icon, InputText, type LoadOptions } from '@vitral/vue';import { ref } from 'vue'; type Status = 'Active' | 'Away' | 'Offline' | 'Blocked'; interface Person {    id: number;    name: string;    city: string;    country: string;    joined: Date;    balance: number;    status: Status;} // A small deterministic generator, so the demo shows the same people every time.let seed = 7;const random = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;const pick = <T,>(list: readonly T[]) => list[Math.floor(random() * list.length)]!; const firstNames = ['Ana', 'Bruno', 'Carla', 'Diego', 'Élise', 'Fatou', 'Gustavo', 'Hanna', 'Iñaki', 'João', 'Kenji', 'Lucía', 'Mateus', 'Noémie', 'Oskar', 'Paula', 'Quentin', 'Raquel', 'Søren', 'Tomás', 'Valéria', 'Wiktor', 'Ximena', 'Zoë'];const lastNames = ['Souza', 'Lima', 'Mendes', 'Ferreira', 'Martin', 'Diallo', 'Rocha', 'Müller', 'Etxeberria', 'Pereira', 'Sato', 'Gómez', 'Nowak', 'Dubois', 'Jónsson', 'Costa', 'Fontaine', 'Álvarez'];const places: [string, string][] = [    ['São Paulo', 'Brazil'],    ['Recife', 'Brazil'],    ['Florianópolis', 'Brazil'],    ['Lisboa', 'Portugal'],    ['Évora', 'Portugal'],    ['Montréal', 'Canada'],    ['Québec', 'Canada'],    ['München', 'Germany'],    ['Köln', 'Germany'],    ['Córdoba', 'Argentina'],    ['Bogotá', 'Colombia'],    ['Kraków', 'Poland'],    ['Zürich', 'Switzerland'],    ['Reykjavík', 'Iceland'],    ['Tōkyō', 'Japan'],    ['Málaga', 'Spain']];const statuses: Status[] = ['Active', 'Active', 'Active', 'Away', 'Offline', 'Blocked']; const people: Person[] = Array.from({ length: 64 }, (_, i) => {    const [city, country] = pick(places);    return {        id: i + 1,        name: `${pick(firstNames)} ${pick(lastNames)}`,        city,        country,        joined: new Date(2019 + Math.floor(random() * 7), Math.floor(random() * 12), 1 + Math.floor(random() * 28)),        balance: Math.round(random() * 2_000_000) / 100 - 2000,        status: pick(statuses)    };}); // A stand-in for a server: it answers in 450 ms with one page, sorted and searched.const requests = ref(0);const remote = createDataSource<Person>({    load: async (options: LoadOptions) => {        requests.value++;        await new Promise((resolve) => setTimeout(resolve, 450));        return queryData(people, options);    }});const remoteFilters = ref({ global: { value: null as string | null, matchMode: 'contains' } });</script> <template>    <DataGrid :data-source="remote" v-model:filters="remoteFilters" :global-filter-fields="['name', 'city', 'country']" aria-label="Remote people" paginator :rows="6" :filter-delay="250" class="demo-table">        <template #header>            <div class="demo-table-bar">                <strong>Server</strong>                <small style="color: var(--vt-text-muted-color)">{{ requests }} requests</small>                <InputText v-model="remoteFilters.global.value" placeholder="Search" aria-label="Search the server" size="small" class="demo-table-search">                    <template #prefix><Icon icon="search" /></template>                </InputText>            </div>        </template>        <Column field="name" header="Name" sortable />        <Column field="city" header="City" sortable />        <Column field="status" header="Status" sortable>            <template #body="{ data }">                <span class="demo-status"><span :class="['demo-dot', `demo-dot-${data.status.toLowerCase()}`]" aria-hidden="true" />{{ data.status }}</span>            </template>        </Column>    </DataGrid></template> <style scoped>.demo-table {    width: 100%;} .demo-table-bar {    display: flex;    align-items: center;    gap: 0.75rem;    flex-wrap: wrap;} .demo-table-search {    margin-inline-start: auto;    width: 16rem;} .demo-status {    display: inline-flex;    align-items: center;    gap: 0.5rem;} .demo-dot {    width: 0.5rem;    height: 0.5rem;    border-radius: 999px;    background: var(--vt-text-muted-color);} .demo-dot-active {    background: var(--vt-success-color);} .demo-dot-away {    background: var(--vt-warn-color);} .demo-dot-blocked {    background: var(--vt-danger-color);}</style>

Vazio e carregando

InvoiceAmount
No invoices this month
NameCity
Ximena DialloSão Paulo
Noémie NowakZürich
Bruno NowakKraków
Quentin GómezTōkyō
Carregando…
<script setup lang="ts">import { Column, DataGrid } from '@vitral/vue'; const people = [    { name: 'Ximena Diallo', city: 'São Paulo' },    { name: 'Noémie Nowak', city: 'Zürich' },    { name: 'Bruno Nowak', city: 'Kraków' },    { name: 'Quentin Gómez', city: 'Tōkyō' }];</script> <template>    <DataGrid :value="[]" aria-label="Empty table" empty-message="No invoices this month" class="demo-table demo-table-half">        <Column field="number" header="Invoice" />        <Column field="amount" header="Amount" align="right" />    </DataGrid>    <DataGrid :value="people" aria-label="Loading table" loading class="demo-table demo-table-half">        <Column field="name" header="Name" />        <Column field="city" header="City" />    </DataGrid></template> <style scoped>.demo-table {    width: 100%;} .demo-table-half {    flex: 1 1 20rem;    width: auto;}</style>

API

Lido de packages/vue/src/components/DataGrid/types.ts, então diz o que o componente aceita de fato.

Props

NomeTipoDescrição
groupBystringAgrupa as linhas por este campo e põe um cabeçalho sobre cada bloco, com uma contagem e um botão de alternar. O agrupamento acontece depois da consulta, então ordenação e filtros continuam decidindo quais linhas existem; `v-model:collapsedGroups` guarda quais delas o leitor fechou.
groupLabel(context: { value: unknown; count: number; rows: any[] }) => stringO que o cabeçalho de um grupo diz; por padrão, o próprio valor.
valueany[]—
dataKeystringUm campo que identifica uma linha, para a seleção e as chaves das linhas.
dataSourceDataSourceLikeAs linhas vêm de uma fonte de dados, local ou remota, em vez de `value`.
lazybooleanNada é calculado aqui: mudanças de ordenação, filtro e página são emitidas (`lazy-load`) para a aplicação responder.
totalRecordsnumberO total de todas as páginas quando `lazy`.
loadingboolean—
paginatorboolean—
rowsPerPageOptionsnumber[]—
pageLinkSizenumber—
paginatorTemplatestring | ('FirstPageLink' | 'PrevPageLink' | 'PageLinks' | 'NextPageLink' | 'LastPageLink' | 'RowsPerPageDropdown' | 'CurrentPageReport')[]—
currentPageReportTemplatestring—
paginatorPosition'top' | 'bottom'—
alwaysShowPaginatorboolean—
sortMode'single' | 'multiple'—
removableSortbooleanUm terceiro clique numa coluna ordenada a tira da ordenação.
globalFilterFieldsstring[]—
filterDisplay'row'`'row'` põe uma caixa de filtro sob o cabeçalho de cada coluna filtrável.
filterDelaynumberMilissegundos a esperar depois da digitação antes de consultar de novo uma tabela lazy ou uma fonte de dados.
selectionMode'single' | 'multiple'—
resizableColumnsbooleanUma alça na borda final de cada coluna, arrastada (ou usada pelo teclado) para definir sua largura.
columnResizeMode'fit' | 'expand'`'fit'` tira a largura da coluna seguinte, mantendo a da tabela; `'expand'` alarga a tabela.
reorderableColumnsbooleanAs colunas podem ser arrastadas pelo cabeçalho para outra ordem, ou movidas com Ctrl e uma seta.
columnTogglebooleanUm botão que abre a lista de colunas, cada uma com um checkbox.
groupstringUm nome que duas tabelas compartilham: elas rolam na horizontal juntas e mantêm um só layout de colunas.
stripedRowsboolean—
showGridlinesboolean—
sizeSize—
scrollableboolean—
scrollHeightstringUma altura CSS, ou `'flex'` para preencher um pai flex. O cabeçalho fica visível.
emptyMessagestring—
rowClass(data: any) => unknown—
tableStylestring | Record<string, string>—
captionstringDá nome à tabela. Oculto a menos que `showCaption`; sem ele, nomeie a tabela com `aria-label`.
showCaptionboolean—

Mais pt, dt e unstyled de BaseProps; veja pass-through e modo sem estilo.

Emits

EventoPayloadDescrição
pageevent: DataGridPageEvent—
column-resizeevent: DataGridColumnEvent & { width: number }Uma coluna foi alargada ou estreitada.
column-reorderevent: DataGridColumnEvent & { order: string[] }Uma coluna foi movida.
column-toggleevent: DataGridColumnEvent & { visible: boolean }Uma coluna foi mostrada ou ocultada.
column-pinevent: DataGridColumnEvent & { side: 'left' | 'right' | null }Uma coluna foi fixada numa borda, ou solta.
sortevent: DataGridSortEvent—
filterevent: DataGridFilterEvent—
lazy-loadevent: LoadOptionsLike—
row-clickevent: DataGridRowClickEvent—
row-selectevent: DataGridRowSelectEvent—
row-unselectevent: DataGridRowSelectEvent—
row-select-allevent: DataGridSelectAllEvent—
row-unselect-allevent: DataGridSelectAllEvent—

Slots

NomeProps do slotDescrição
default—Os `<Column>`s.
header——
footer——
empty——
loadingicon——
paginatorstart(props: DataGridPageEvent)—
paginatorend(props: DataGridPageEvent)—