Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

DataTable

DataTable mostra linhas e colunas como uma planilha e rola nas duas direções por qualquer quantidade de ambas. Somente as células visíveis são criadas, de modo que uma tabela de um milhão de linhas e dez mil colunas rola como uma pequena. Linhas, colunas ou células podem ser selecionadas, com o ponteiro e o teclado; as colunas têm larguras fixas, são redimensionadas arrastando a borda, movidas arrastando o título, ficam à esquerda enquanto as outras rolam e podem ser ordenadas. Os títulos podem ficar sob rótulos de grupo, e uma célula ou um título pode ser desenhado pelo app.

el-data-table.tsx
el-data-table.tsxLive
import { mount, signal, Text, View } from "nsh";
import { DataTable, type DataTableColumn, type DataTableSort } from "nsh/base";
import { dataTableClasses, dataTableFitPadding, Log, Segmented, Split, ui, wide } from "./palette";

const NAMES = ["Ada", "Alan", "Grace", "Linus", "Margaret", "Dennis", "Barbara", "Ken"];
const CITIES = ["Oslo", "Lima", "Kyoto", "Cairo", "Perth", "Quito", "Turin", "Hanoi"];

type Person = { id: number; name: string; city: string; score: number };

const people: Person[] = Array.from({ length: 2000 }, (_, i) => ({
  id: i + 1,
  name: `${NAMES[i % NAMES.length]} ${i + 1}`,
  city: CITIES[(i * 7) % CITIES.length],
  score: (i * 37) % 1000,
}));

const columns: DataTableColumn[] = [
  { key: "id", title: "ID", width: 70, align: "end", fixed: true, sortable: true },
  { key: "name", title: "Name", width: 150, sortable: true },
  { key: "city", title: "City", width: 120, sortable: true },
  { key: "score", title: "Score", width: 90, align: "end", sortable: true },
];

// Sorting is the app's: the table reports the next sort (`onSortChange`),
// the app orders its rows and sets `sort`.
function App() {
  const mode = signal<"row" | "cell">("row");
  const sort = signal<DataTableSort | undefined>(undefined);
  const rows = signal<Person[]>(people);
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));

  const sortBy = (next: DataTableSort | undefined) => {
    sort.set(next);
    if (!next) return rows.set(people);
    const key = next.key as keyof Person;
    const sign = next.direction === "descending" ? -1 : 1;
    rows.set([...people].sort((a, b) => (a[key] < b[key] ? -sign : a[key] > b[key] ? sign : 0)));
  };

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>data table</Text>
      <Text style={{ color: ui().muted }}>Press a title to sort, drag its edge to resize or the title to move it</Text>

      <Split
        grow
        width={680}
        inspector={
          <>
            <Segmented label="selection" options={["row", "cell"] as const} value={mode()} onChange={(v) => mode.set(v)} />
            <Log title="events" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 360 : 320 }}>
          <DataTable
            columns={columns.map((column) => ({ ...column, fitPadding: dataTableFitPadding }))}
            classes={dataTableClasses}
            data={rows()}
            rowHeader={false}
            stripe
            cellSelectable={mode() === "cell"}
            sort={sort()}
            onSortChange={({ sort: next }) => {
              sortBy(next ?? undefined);
              log(`onSortChange: ${next ? `${next.key} ${next.direction}` : "none"}`);
            }}
            onSelectionChange={({ selection }) => log(`onSelectionChange: ${selection ? JSON.stringify(selection) : "none"}`)}
            onColumnResize={({ key, width }) => log(`onColumnResize: ${key} ${Math.round(width)}`)}
            style={{ flex: 1 }}
          />
        </View>
      </Split>
    </View>
  );
}

mount(App);

DataTable fornece layout, alinhamento, virtualização e interação. O app dá às suas partes o padding, as cores, as bordas e o feedback de seleção por meio de classes e styles. Rust cria uma com nsh_base::data_table(), TSX com <DataTable> de nsh/base.

use nsh_base::{DataTableColumn, data_table};
use nsh_core::prelude::*;

fn column(key: &str, title: &str) -> DataTableColumn {
    let mut column = DataTableColumn::default();
    column.key = key.into();
    column.title = Some(title.into());
    column
}

fn table() -> impl IntoElement {
    data_table()
        .columns(vec![column("name", "Name"), column("city", "City")])
        .flex_1()
}

A tabela preenche a caixa que o app lhe dá (flex-1, uma altura) e rola dentro dela: na vertical pelas linhas e na horizontal pelas colunas, com barras de rolagem (scrollbar={false} as oculta, bounces={false} impede o efeito de elasticidade nas extremidades onde a plataforma o tem).

#Linhas e colunas

data são as linhas: objetos cujos campos são nomeados pelos keys das colunas, ou arrays que as colunas indexam por posição. Números, booleanos e strings são exibidos como texto. Rust fornece as linhas um campo por vez, cada um de um tipo: DataTableRows::new(2).texts("symbol", ["AAPL", "MSFT"]).numbers("change", [1.2, -0.4]). Uma coluna é um DataTableColumn:

Campo Descrição
key Identifica a coluna, única entre as colunas: o campo das linhas que suas células mostram e o nome que os eventos e a ref usam.
title O texto do seu título. Padrão: seu key.
width Sua largura, em pontos. Padrão 120.
minWidth, maxWidth A menor e a maior largura que um arrasto ou um ajuste lhe dá. Padrão 20 e 1200.
fitPadding Espaço extra para o padding, os ícones ou os badges do app, somado ao texto mais largo do título ou das células ao ajustar a coluna.
align "start" (o padrão), "center" ou "end", para números. Vale tanto para as células quanto para o título.
sortable Seu título tem um botão de ordenação que solicita uma ordenação (veja Ordenação).
fixed Fica à esquerda enquanto as outras colunas rolam sob ela. As colunas fixas vêm antes das outras, na sua ordem.
resizable, movable Se um arrasto de sua borda a redimensiona e se um arrasto de seu título a move. Padrão: resizableColumns e movableColumns da tabela.
selectable Se participa da seleção. Uma coluna de botões ou checkboxes define false. Padrão true.
cellClass Classes das células da coluna, depois das da parte cell: paddings ou alinhamento de uma única coluna.
headerClass Classes do título da coluna, depois das da parte headerCell.
positiveClass Classes das células da coluna que contêm uma alta: um número acima de zero ou um texto que começa com +.
negativeClass Classes das células da coluna que contêm uma queda: um número abaixo de zero ou um texto que começa com -.

rowHeight é a altura de uma linha do corpo (padrão 32) e headerHeight a altura de uma linha de título (padrão: a altura da linha). rowHeader adiciona uma coluna de números de linha à esquerda, que seleciona linhas. stripe dá às linhas ímpares a parte rowOdd.

#Rótulos de grupo

groups coloca rótulos acima das colunas, começando pelo nível mais baixo. Cada nível é uma lista de { label, span } cujos spans somam o número de colunas, ou o do nível abaixo.

<DataTable
  columns={columns}
  data={rows}
  groups={[
    [{ label: "Person", span: 2 }, { label: "Result", span: 1 }],
  ]}
  class="flex-1"
/>

#Qualquer quantidade de linhas

A tabela pede as linhas por posição, e o app as fornece. rowCount é o número total de linhas, data as linhas que o app mantém e dataStart a posição da primeira delas. Linhas fora de data ficam vazias até o app fornecê-las. onVisibleRowsChange informa as linhas visíveis, { range: { start, end } }, a cada passo de rolagem, e é o lugar para buscar a página ao redor delas.

const ROWS = 1_000_000;
const page = signal({ start: 0, rows: load(0) });

<DataTable
  columns={columns}
  data={page().rows}
  dataStart={page().start}
  rowCount={ROWS}
  onVisibleRowsChange={({ range: { start, end } }) => {
    // Keep the rows in view inside the page held.
    const held = page();
    if (start < held.start || end > held.start + held.rows.length) {
      const first = Math.max(0, start - 200);
      page.set({ start: first, rows: load(first) });
    }
  }}
  class="flex-1"
/>

Para uma lista que cresce no final, defina hasMore: onLoadMore dispara uma vez para cada número de linhas quando as linhas visíveis chegam a loadMoreThreshold (padrão 20) da última. Enquanto loading for true, ele espera, e o slot loadingView aparece sobre as linhas. O slot empty aparece quando não há linhas.

Passando de alguns milhões de pontos de conteúdo, as barras de rolagem abrangem um intervalo escalonado, de modo que um milhão de linhas ainda rola até cada linha.

#Seleção

Três chaves decidem o que um pressionamento seleciona, como no gpui-component:

Prop Um pressionamento seleciona selection
rowSelectable (padrão true) Uma linha: qualquer uma de suas células ou o cabeçalho de linha { row }
columnSelectable (padrão true) Uma coluna: seu título { column }
cellSelectable (padrão false) Uma célula; o cabeçalho de linha (exibido por padrão com ela) seleciona linhas { row, column }

Com cellSelectable e o cabeçalho de linha oculto (rowHeader={false}), pressionar a célula selecionada de novo seleciona sua linha. Um pressionamento com Shift ou com a tecla de comando da plataforma seleciona uma linha somente enquanto selectOnModifierPress (padrão true), de modo que um app que mantém sua própria seleção de várias linhas trata esses pressionamentos por conta própria: onRowPress informa cada pressionamento no corpo com sua linha, a coluna sob o ponteiro, os cliques em uma linha e as teclas modificadoras pressionadas. Uma coluna cujo selectable é false não seleciona nem a si mesma nem suas células.

As linhas contam a partir de 0 nos dados, e uma coluna é seu índice entre columns, qualquer que seja a ordem para a qual o usuário as moveu. onSelectionChange informa o que o usuário selecionou (null para nada). A prop selection define a seleção a partir do app; deixe-a sem definir e a tabela a mantém. onDoublePress informa uma linha, uma célula ou um título pressionado duas vezes, e onContextMenu um pressionamento secundário ou longo, com o ponto onde abrir o Menu do app. Ambos nomeiam a célula sob o ponteiro (seu row e column), o número de uma linha (somente row) ou um título (somente column). Até o próximo pressionamento, a célula para a qual um menu foi pedido tem a parte cellActive, ou sua linha tem a parte rowActive onde as células não selecionam.

Leitores de tela recebem uma grade (nomeada por accessibilityLabel) de cabeçalhos de coluna e células, e as selecionadas são anunciadas como selecionadas.

#Teclado

Pressionar a tabela, ou chegar a ela com Tab (macOS, web), dá a ela o teclado até outro pressionamento, até o foco ir para outro lugar ou até blur(); focus() o dá a partir do app. A tabela é uma parada do Tab: dentro dela o Tab move a seleção, e Escape devolve o teclado.

Teclas O que fazem
Setas Movem a seleção por uma linha, uma coluna ou uma célula, rolando-a para a área visível.
Tab, Shift+Tab Movem para a próxima ou a anterior coluna.
Page Up, Page Down Movem por uma página de linhas.
Home, End Movem para a primeira ou a última.
Escape Limpa a seleção e depois devolve o teclado.
Atalho de cópia Coloca o texto da seleção na área de transferência (copy()).

loopSelection (padrão true) faz as setas darem a volta nas extremidades.

#Ordenação

Uma coluna sortable tem um botão de ordenação no fim do título, um pequeno botão ghost esmaecido enquanto a coluna não está ordenada. Pressioná-lo informa a próxima ordenação ({ key, direction }) no ciclo de sem ordem, crescente, decrescente e sem ordem de novo, com onSortChange({ sort }) (null para limpar). A tabela não reordena as linhas: o app ordena seus dados, ou pede ao servidor as linhas na nova ordem, e define sort para que o título a mostre. Como o app é dono da ordem, a ordenação funciona do mesmo modo sobre um milhão de linhas que o app nunca mantém.

<DataTable
  columns={columns}
  data={rows()}
  sort={sort()}
  onSortChange={({ sort: next }) => {
    sort.set(next ?? undefined);
    rows.set(sorted(people, next));
  }}
  class="flex-1"
/>

#Redimensionar e mover colunas

Um arrasto da borda final de uma coluna a redimensiona dentro de minWidth e maxWidth, e onColumnResize({ key, width }) informa cada passo. Pressionar a borda duas vezes seguidas ajusta a coluna ao seu texto e título mais largos. Um arrasto de um título move a coluna, e uma linha mostra onde ela será solta; onColumnMove informa { key, from, to, order }, sendo order os keys de todas as colunas na ordem exibida. As colunas fixas permanecem entre as fixas. resizableColumns e movableColumns desativam ambos para a tabela, e resizable e movable de uma coluna prevalecem sobre eles.

#Dados ao vivo

Uma tabela de cotações muda muitas vezes por segundo. O app formata as linhas visíveis e uma margem ao redor delas (ele as conhece por onVisibleRowsChange) e as entrega como data de dataStart, de modo que uma atualização custa o que está na tela, qualquer que seja o número de linhas. Altas e quedas tiram suas cores de positiveClass e negativeClass da coluna, aplicadas nativamente às células cujo valor está acima ou abaixo de zero (ou cujo texto começa com + ou -).

const tone = { positiveClass: "text-green-600", negativeClass: "text-red-600" };
const columns: DataTableColumn[] = [
  { key: "symbol", title: "Symbol", fixed: true },
  { key: "price", title: "Price", align: "end", sortable: true },
  { key: "change", title: "Chg%", align: "end", ...tone },
];

Linhas e colunas são slots que a tabela cria uma vez e mantém: a rolagem lhes dá outras linhas e colunas para mostrar, de modo que uma rolagem, por mais longa que seja, só altera textos e posições.

#Desenhar células e títulos

renderCell desenha as células próprias do app onde uma coluna não é apenas texto (um badge, um botão). É chamado somente para as células visíveis, com um DataTableCell: row, column, key, text (o que a tabela mostraria) e selected. O app busca seus próprios dados por row. renderHeaderCell desenha um título ao longo de sua célula, recebendo um DataTableHeaderCell: column, key, title, sort (a direção, ou nenhuma) e selected.

<DataTable
  columns={columns}
  data={rows()}
  renderCell={(cell) =>
    cell.key === "status" ? (
      <View class={cell.text === "open" ? "rounded bg-green-500/20 px-2" : "rounded bg-muted px-2"}>
        <Text class="text-xs">{cell.text}</Text>
      </View>
    ) : (
      <Text class="px-2 text-sm">{cell.text}</Text>
    )
  }
  class="flex-1"
/>

fontFamily e fontSize (padrão 14) definem a fonte das células, na qual a tabela também mede os textos para ajustar as colunas.

#A ref

ref fornece um DataTableRef. Um método que recebe uma linha, uma coluna ou uma chave de coluna que a tabela não tem não altera nada e registra um aviso com o nome do método e o valor; fitColumn é rejeitado com um TypeError para uma chave desconhecida.

Método Descrição
scrollToRow(row, animated?) Rola uma linha para a área visível.
scrollToColumn(key, animated?) Rola uma coluna para a área visível.
selection() O que está selecionado, null para nada.
select(selection) Seleciona (null: nada) e traz para a área visível, sem onSelectionChange.
visibleRows(), visibleColumns() As linhas e as colunas roláveis visíveis, como { start, end } (end excluído).
columnWidths() As larguras atuais, na ordem de columns.
setColumnWidth(key, width) Define a largura de uma coluna, dentro de seus limites.
fitColumn(key) Define uma coluna tão larga quanto seu texto mais largo e seu título precisarem, entre as linhas visíveis e até mil a mais ao redor delas. Retorna a largura.
dump(start?, end?) Os textos das linhas informadas (padrão: todas as mantidas) e dos títulos, como exibidos: { headers, rows }, para exportação.
copy() Coloca o texto da seleção na área de transferência: as células de uma linha separadas por tabulações, as de uma coluna por quebras de linha, o texto de uma célula.
focus(), blur() Dá o teclado à tabela ou o retira.
menuTarget(), clearMenuTarget() Para onde o último menu de contexto foi pedido ({ row?, column? }, marcado por cellActive / rowActive) e a remoção dessa marca quando o menu do app fecha.

#Estilizar as partes

class e style estilizam a caixa da tabela. classes recebe classes Tailwind por parte e styles estilos inline por parte; as partes fornecem a estrutura e o alinhamento da tabela; suas classes fornecem a aparência, e styles prevalecem sobre as classes.

<DataTable
  columns={columns}
  data={rows()}
  stripe
  class="flex-1 rounded-lg border border-border"
  classes={{
    header: "bg-secondary",
    headerCell: "pl-2 pr-1",
    headerContent: "gap-1",
    cell: "px-2",
    resizeHandle: "bg-border group-hover:bg-primary",
    sortAscending: "text-muted-foreground opacity-60 selected:text-foreground selected:opacity-100",
    sortDescending: "text-muted-foreground opacity-60 selected:text-foreground selected:opacity-100",
    rowSelected: "bg-primary/10",
    rowHover: "bg-muted/50",
    fixedEdge: "bg-border",
  }}
/>
Parte O que é
root A caixa da tabela: sua borda, seu raio e seu fundo
header O cabeçalho ao longo da tabela, com suas linhas de rótulos de grupo e títulos
groupCell Um rótulo de grupo acima das colunas que abrange
headerCell A célula de título de cada coluna
headerContent A linha de layout do rótulo e do botão de ordenação; defina o espaçamento entre eles aqui
headerCellDragging O título arrastado, depois de headerCell
headerCellSorted O título da coluna pela qual a tabela está ordenada, depois de headerCell
headerCellSelected O título da coluna selecionada, depois de headerCell
sortIndicator O botão de ordenação de uma coluna ordenável
sortAscending, sortDescending Seus dois chevrons; selected: vale para a direção ordenada
resizeHandle A linha na borda final de uma coluna que um arrasto redimensiona
moveIndicator A linha onde uma coluna arrastada será solta
row Cada linha do corpo
rowOdd As linhas ímpares, depois de row (stripe)
rowHover A linha sobre a qual o ponteiro está, depois de row
rowSelected A linha selecionada, depois de row
rowActive A linha para a qual um menu de contexto foi pedido onde as células não selecionam, até o próximo pressionamento, depois de row
cell Cada célula do corpo
cellSelected A célula selecionada, depois de cell
cellActive A célula para a qual um menu de contexto foi pedido, até o próximo pressionamento, depois de cell
cellColumnSelected As células da coluna selecionada, depois de cell
rowHeader As células do cabeçalho de linha, os números de linha
rowHeaderCorner O canto acima dos cabeçalhos de linha
fixedEdge A linha na borda final das colunas fixas, sobre as colunas que rolam sob elas
empty A caixa do slot empty

#Propriedades

Prop Rust Tipo Padrão Descrição
columns .columns(..) DataTableColumn[] [] As colunas, da esquerda para a direita. As fixas vêm primeiro.
data .data(..) Row[] [] As linhas: objetos indexados pela coluna key, ou arrays indexados por posição.
rowCount .row_count(..) number as linhas mantidas Quantas linhas existem no total.
dataStart .data_start(..) number 0 O índice da primeira linha de data.
rowClasses .row_classes(..) string[] [] Classes das linhas informadas, pela posição delas em data: a aparência própria de uma linha, sem chamar o JS por linha.
rowHeight .row_height(..) number 32 A altura de uma linha do corpo, em pontos.
headerHeight .header_height(..) number a altura da linha A altura de uma linha de título.
groups .groups(..) DataTableColumnGroup[][] [] Rótulos acima das colunas, do nível mais baixo para o mais alto.
sort .sort(..) DataTableSort Pelo que os dados estão ordenados, exibido no título.
rowSelectable .row_selectable(..) boolean true Toques e teclas selecionam linhas.
columnSelectable .column_selectable(..) boolean true O toque em um título seleciona a coluna dele, e as teclas horizontais se movem entre colunas.
cellSelectable .cell_selectable(..) boolean false Toques e teclas selecionam células.
selectOnModifierPress .select_on_modifier_press(..) boolean true Um toque com Shift ou a tecla Command pressionada seleciona uma linha.
selection .selection(..) DataTableSelection O que está selecionado, quando o app o define.
loopSelection .loop_selection(..) boolean true As teclas de seta dão a volta nas extremidades.
accessibilityLabel .accessibility_label(..) string nenhum Como os leitores de tela chamam a tabela.
rowHeader .row_header(..) boolean exibido com cellSelectable Uma coluna de números de linha à esquerda.
stripe .stripe(..) boolean false As linhas ímpares recebem a parte rowOdd.
resizableColumns .resizable_columns(..) boolean true Arrastar a borda de uma coluna a redimensiona.
movableColumns .movable_columns(..) boolean true Arrastar um título move a coluna dele.
loading .loading(..) boolean false A tabela está carregando: onLoadMore espera e loadingView é exibido.
hasMore .has_more(..) boolean false Há mais linhas a carregar.
loadMoreThreshold .load_more_threshold(..) number 20 A quantas linhas da última o evento onLoadMore é disparado.
fontFamily .font_family(..) string herdada A fonte das células.
fontSize .font_size(..) number 14 O tamanho da fonte das células, em pontos.
scrollbar .scrollbar(..) boolean true As barras de rolagem.
verticalScrollbar, horizontalScrollbar .vertical_scrollbar(..), .horizontal_scrollbar(..) boolean scrollbar A barra de rolagem de um eixo (a horizontal é a barra dos desktops sob a tabela).
bounces .bounces(..) boolean true Se a rolagem “quica” nas extremidades, nas plataformas que fazem isso.
renderCell .render_cell(..) (cell: DataTableCell) => JSX.Element Desenha as células visíveis.
renderHeaderCell .render_header_cell(..) (cell: DataTableHeaderCell) => JSX.Element Desenha os títulos.
empty .empty(..) JSX.Element O que é exibido quando não há linhas.
loadingView .loading_view(..) JSX.Element O que é exibido sobre as linhas enquanto loading.
classes .classes(..) DataTableClasses {} Classes do Tailwind por parte.
styles .styles(..) DataTableStyles {} Estilos inline por parte, sobrepostos às classes.
class, style métodos de estilo string, StyleProp<ViewStyle> A caixa da tabela.

#Eventos

Evento Rust Descrição
onSelectionChange .on_selection_change(|selection: &Option<DataTableSelection>| ..) O usuário alterou a seleção: { selection }, um { row?, column? } ou null.
onRowPress .on_row_press(|event: &DataTablePress| ..) Uma linha ou célula foi tocada: { row, column?, clicks, shift, control, alt, platform }.
onDoublePress .on_double_press(|target: &DataTableTarget| ..) Uma linha, célula ou título foi tocado duas vezes: { target }, um { row?, column? }.
onContextMenu .on_context_menu(|event: &DataTableContextMenu| ..) Um menu de contexto foi solicitado: { row?, column?, x, y }, o ponto na view raiz.
onSortChange .on_sort_change(|sort: &Option<DataTableSort>| ..) Um botão de ordenação foi tocado: { sort }, a próxima ordenação { key, direction } ou null.
onColumnResize .on_column_resize(|key: &SharedString, width: &Points| ..) Uma coluna foi redimensionada ({ key, width }), a cada passo de um arraste e mais uma vez quando um ajuste a define.
onColumnMove .on_column_move(|event: &DataTableColumnMove| ..) Uma coluna foi movida: { key, from, to, order }.
onVisibleRowsChange .on_visible_rows_change(|range: &DataTableRange| ..) As linhas visíveis mudaram: { range }, um { start, end }. Busque as linhas a partir daqui.
onVisibleColumnsChange .on_visible_columns_change(|range: &DataTableRange| ..) As colunas roláveis visíveis mudaram: { range }.
onLoadMore .on_load_more(|| ..) As linhas visíveis chegaram a loadMoreThreshold da última enquanto hasMore e não loading.

#Relacionados

  • Editor e DiffView: os outros componentes base com muitos dados.
  • Menu: o menu a abrir a partir de onContextMenu.
  • nsh/base: os outros componentes base.