Cansado de procurar por aí e precisando entregar uma aplicação que emulasse o máximo possível de funcionalidade que o Microsoft Excel tem (dentro do que era utilizado pelos usuários, nada muito complexo), desenvolvi um componente de tabela utilizando Vue 3, que cumpriu bem seu objetivo.
Te interessou? Veja aqui um exemplo.
Você pode também navegar pelo código-fonte do componente.
- ← ↑ → ↓ PgUp PgDown: navega entre as células
- Tab: navega para a próxima célula. Se estiver no modo de edição, salva as alterações e sai do modo de edição
- Shift + Tab: navega para a célula anterior. Se estiver no modo de edição, salva as alterações e sai do modo de edição
- Home: navega para o início da linha
- End: navega para o final da linha
- Ctrl + Home: navega para a célula da primeira linha e primeira coluna
- Ctrl + End: navega para a célula da última linha e última coluna
- Shift + ← ↑ → ↓: seleciona as células enquanto navega
- Shift + Home: seleciona da célula atual até o início da linha
- Shift + End: seleciona da célula atual até o fim da linha
- Ctrl + Shift + Home: seleciona da célula atual até a célula da primeira linha e primeira coluna
- Ctrl + Shift + End: seleciona da célula atual até a célula da última linha e última coluna
- Ctrl + Shift + +: insere uma nova linha
- Ctrl + A: seleciona todas as células da tabela, exceto cabeçalho
- Ctrl + X: recorta as células selecionadas
- Ctrl + C: copia as células selecionadas
- Ctrl + V: cola da área de transferência, ou da área marcada na tabela
- F2: ativa a edição da célula sem apagar o conteúdo existente
- Comece a digitar: ativa a edição da célula limpando o conteúdo existente
- Enter: salva as alterações e sai do modo de edição
- Esc: cancela a edição da célula, ou a seleção da área de transferência
- Delete: exclui o conteúdo das células selecionadas
- Shift + Delete: exclui as linhas selecionadas
- Espaço em uma coluna do tipo boolean (com checkboxes): marca/desmarca todas as checkboxes das células selecionadas
- Clique: seleciona a célula sob o mouse. Se outra célula estiver no modo de edição, salva as alterações e sai do modo de edição
- Shift + clique: seleciona um intervalo de células
- Clique duplo: inicia o modo de edição da célula sob o mouse
Para funcionar minimamente, o componente exige que duas propriedades estejam especificadas: colunas
e linhas
. Os exemplos aqui demonstrados utilizarão TypeScript e a Composition API.
<script setup lang="ts">
import Tabela, { type Coluna } from '@/components/Tabela.vue';
import { reactive } from 'vue';
let colunas: Coluna[] = [
{
Nome: "coluna1",
Descricao: "Coluna 1",
Tipo: "string"
}
];
let linhas: Record<string, any>[] = reactive([
{
coluna1: "Olá, mundo!"
}
]);
</script>
<template>
<Tabela :colunas="colunas" :linhas="linhas" />
</template>
O exemplo acima irá renderizar uma tabela com apenas uma linha e uma coluna. É um bom começo, mas podemos fazer melhor que isso, certo?
Vamos começar analisando o tipo Coluna
que define as colunas que iremos passar para nosso componente:
type Coluna = {
Nome: string;
Descricao?: string;
Tipo: "string" | "number" | "boolean";
AtributosCabecalho?: Record<string, any>;
AtributosCelula?: Record<string, any>;
Validacao?: (valor: any) => any | false;
Conversao?: (valor: any, converterPara?: string) => any;
}
- Nome: nome da propriedade em cada objeto da coleção
linhas
que aparecerá nessa coluna - Tipo: tipo da coluna. Cada tipo possui seu comportamento específico:
- string: tipo padrão. Não faz nenhuma validação de dados, e no modo de edição utiliza um
<input type="text" />
. - number: tipo número. A princípio os dados não são convertidos para números válidos, mas após qualquer alteração é utilizada a conversão de valor utilizando-se
Number()
(por exemplo,Number("42")
). Caso essa conversão resulte emNaN
(por exemplo, como resultado deNumber("abc")
ouNumber("")
), será atribuído o valor padrãonull
, que é o valor padrão para representar a ausência de valor numérico. No modo de edição, utiliza um<input type="number" />
. - boolean: tipo booleano. É representado por um
<input type="checkbox" />
, estando checado caso o valor seja equivalente atrue
e em branco caso o valor seja equivalente afalse
. A princípio os dados não são convertidos para booleano, mas assim que há alguma alteração, é convertido para o equivalente booleano através de!!
(por exemplo,!!"Olá, mundo!"
é convertido paratrue
, e!!0
é convertido parafalse
).
- string: tipo padrão. Não faz nenhuma validação de dados, e no modo de edição utiliza um
-
Descricao: valor a ser mostrado no cabeçalho da coluna. Caso seja omitido, a célula do cabeçalho não será renderizada. Caso ela deva ser renderizada sem nenhum conteúdo dentro, basta especificar uma string vazia (
""
). -
AtributosCabecalho e AtributosCelula: objeto cujas propriedades se tornarão atributos da célula do cabeçalho e da célula, respectivamente. Por exemplo:
let colunas: Coluna[] = [ { Nome: "coluna1", Descricao: "Coluna 1", Tipo: "string", AtributosCabecalho: { class: [ "cabecalho", "coluna-1" ], // o Vue também aceita string e objeto, consulte a documentação para saber como utilizar style: "text-align: center", // o Vue também aceita array e objeto, consulte a documentação para saber como utilizar "aria-label": "Coluna 1" }, AtributosCelula: { class: { celula: true, realce: tipoTabela === "realce" }, style: { "text-align": "right", "background-color": "#c00" } } } ];
-
Validacao: função de validação de dados. Será chamada sempre que o valor da célula estiver prestes a ser atualizado. Deverá retornar o valor recebido (poderá formatar esse valor caso necessário, ou
false
caso seja inválido. Se o valor retornado forfalse
, não haverá atualização do valor da célula. Se ela estiver no modo de edição, a edição será cancelada. -
Conversao: função de conversão de dados. Será chamada sempre que for necessária conversão de dados (por exemplo, de
number
parastring
). Caso o parâmetroconverterPara
seja fornecido, deverá efetuar a conversão do parâmetrovalor
para o tipo especificado. Caso não seja fornecido, o parâmetrovalor
deverá ser convertido para tipo da coluna. É possível também criar associações de/para personalizada de dados. Por exemplo, denumber
parastring
sendo 1 => "Um", 2 => "Dois", etc, e vice-versa.
O tipo ReferenciaCelula
serve como auxiliar para representar uma referência de célula, com linha e coluna. É instanciado da seguinte forma:
let celulaAtual = new ReferenciaCelula(3, 2);
No exemplo acima, celulaAtual
contém uma instância de ReferenciaCelula
representando a célula na quarta linha e terceira coluna. Isso porque tanto as linhas quanto as colunas iniciam a contagem em 0. Dessa forma, a célula do canto superior direito da tabela (desconsiderando-se o cabeçalho) pode ser referenciada por new ReferenciaCelula(0, 0)
, ou seja, a célula da primeira linha e da primeira coluna.
Para acessar os dados de uma ReferenciaCelula
, existem duas propriedades: Linha
e Coluna
. Elas podem ser alteradas como demonstrado no exemplo abaixo:
let celulaAtual = new ReferenciaCelula(3, 2);
console.log(`Linha: ${celulaAtual.Linha}, Coluna: ${celulaAtual.Coluna}`); // Linha: 3, Coluna: 2
celulaAtual.Linha = 8;
celulaAtual.Coluna = 1;
Ela também possui alguns métodos úteis:
let celula1 = new ReferenciaCelula(0, 0);
let celula2 = new ReferenciaCelula(2, 2);
// O método set atribui linha e coluna de acordo com o parâmetro passado
celula1.set(celula2); // celula1 agora tem a mesma linha e coluna que celula2
celula1.set(2, 2); // o método set também aceita dois parâmetros de linha e coluna, respectivamente
// O método equals verifica se ambas as referências apontam para a mesma célula
celula1.equals(celula2); // retorna true
celula1.set(0, 0);
celula1.equals(celula2); // retorna false
ReferenciaCelula.equals(celula1, celula2); // versão estática do método, aceita dois parâmetros
let celula3 = new ReferenciaCelula(3, 0);
// Os métodos min e max aceitam 1 ou mais parâmetros
let celulaMinima = ReferenciaCelula.min(celula1, celula2, celula3); // retorna uma ReferenciaCelula com Linha 2 e Coluna 0
let celulaMaxima = ReferenciaCelula.max(celula1, celula2, celula3); // retorna uma ReferenciaCelula com Linha 3 e Coluna 2
// O método inRange verifica se uma ReferenciaCelula está contida dentro do retângulo delimitado pelos parâmetros passados
console.log(celula2.inRange(celulaMinima, celulaMaxima)); // true
console.log(celula2.inRange(celula1, celula3)); // true, pois não importa em quais cantos do retângulo estão as células
Apesar de já termos muitas opções de controlar cada coluna da tabela, há atributos adicionais que podemos colocar no componente para personalizá-lo ainda mais:
Atributo | Tipo | Descrição |
---|---|---|
atributos-cabecalho | (coluna: Coluna, numeroColuna: number) => Record<string, any> | undefined |
Função que define os atributos do cabeçalho dinamicamente, de acordo com a coluna passada pelos parâmetros. |
atributos-linha | (linha: Record<string, any>, numeroLinha: number) => Record<string, any> | undefined |
Função que define os atributos da linha, de acordo com a linha passada pelos parâmetros. |
atributos-celula | (linha: Record<string, any>, numeroLinha: number, numeroColuna: number) => Record<string, any> | undefined |
Função que define os atributos da célula, de acordo com a linha e coluna passadas pelos parâmetros. |
celula-atual | ReferenciaCelula |
Para obter o valor da célula atual, ou controlá-lo, basta passar uma ReferenciaCelula nesse atributo. |
inicio-selecao | ReferenciaCelula |
Para obter o valor do início da seleção das células, ou controlá-lo, basta passar uma ReferenciaCelula nesse atributo. |
somente-leitura | boolean |
Caso seja true , impede alterações nos dados da tabela, mas não impede interações que não envolvam alteração de dados. |
confirmacao-exclusao-linhas | (celulaAtual: ReferenciaCelula, inicioSelecao: ReferenciaCelula) => Promise<void> |
Função que deve retornar uma promise que se resolvida confirma a exclusão das linhas selecionadas, e se rejeitada cancela a ação de exclusão. Pressupõe-se que ela exiba um diálogo com mensagem para o usuário, e através de sua interação ele escolha por continuar ou cancelar a ação. |
confirmacao-exclusao-conteudo | (celulaAtual: ReferenciaCelula, inicioSelecao: ReferenciaCelula) => Promise<void> |
Função que deve retornar uma promise que se resolvida confirma a exclusão do conteúdo das células selecionadas (ou seja, ficarão em branco), e se rejeitada cancela a ação de exclusão. Pressupõe-se que ela exiba um diálogo com mensagem para o usuário, e através de sua interação ele escolha por continuar ou cancelar a ação. |
colar-celulas | (destino: ReferenciaCelula, dados: string[][] | null, origemInicial: ReferenciaCelula, origemFinal: ReferenciaCelula, removerOrigem: boolean) => void | false |
Função que executa a ação de colar as células, de acordo com os parâmetros especificados. O parâmetro dados representa dados externos, e quando não é null significa que os parâmetros seguintes estão vazios. Caso o parâmetro dados esteja vazio, os parâmetros seguintes estão preenchidos e representam a localização dos dados dentro da própria tabela. Caso retorne false, o event de colar da área de transferência é cancelado. |
propriedades-estilo-copiar | string[] |
Define quais propriedades do estilo (por exemplo, fonte, cor das bordas, cor do fundo, etc) das células selecionadas deverão ser copiados para a área de transferência juntamente com os dados. Ao colar em um editor que aceite dados formatados como o Microsoft Excel, por exemplo, esses estilos vêm junto. |
Evento | Assinatura do event handler | Descrição |
---|---|---|
update:linhas | (linhas: Record<string, any>[]) => void |
Disparado sempre que há alterações nos dados das linhas da tabela. |
update:celulaAtual | (celulaAtual: ReferenciaCelula) => void |
Disparado sempre que há alteração na referência da célula atual. |
update:inicioSelecao | (inicioSelecao: ReferenciaCelula) => void |
Disparado sempre que há alteração no início de seleção de células. |
update:celula | (evento: Event, valor: string, numeroLinha: number, numeroColuna: number) => void |
Disparado sempre que há um evento que altera o valor da célula enquando a mesma está no modo de edição. Por exemplo, em um <input type="text" /> , equivale ao evento input . |
O compomente Tabela
permite que você personalize algumas partes de seu template através de slots. São eles:
Slot | Descrição |
---|---|
cabecalho | Permite personalizar o cabeçalho da tabela. |
edicao-celula-string | Mostrado no modo de edição de célula do tipo string . |
edicao-celula-number | Mostrado no modo de edição de célula do tipo number . |
edicao-celula-boolean | Mostrado no modo de edição de célula do tipo boolean . |
A seguir, o funcionamento de cada slot:
-
cabecalho: você pode definir sua própria lógica de renderização de células do cabeçalho. Para auxiliar nesse processo, é passado ao slot o seguinte objeto:
{ colunas: Coluna[] }
Dessa forma, é possível renderizar as colunas utilizando-se um
v-for
. -
edicao-celula-string, edicao-celula-number e edicao-celula-boolean: esses três slots funcionam da mesma maneira: são exibidos somente quando o modo de edição está ativado, somente na célula selecionada. Eles recebem o seguinte objeto:
{ linha: number, coluna: number, nomeColuna: string, dados: Record<string, any>, finalizarEdicaoCelula: () => void, atualizarValorCelula: (valor: any) => void }
A seguir, a descrição de cada propriedade:
- linha: número da linha de dados (começando com 0)
- coluna: número da coluna de dados (começando com 0)
- nomeColuna: nome da propriedade do objeto
dados
que contém o valor da célula - dados: objeto que contém os dados da linha da célula
- finalizarEdicaoCelula: método que deve ser chamado para sair do modo de edição. Por exemplo, em um
<input type="text" />
esse método seria chamado no eventoblur
. - atualizarValorCelula: método que deve ser chamado para alterar o valor da célula atual. Por exemplo, em um
<input type="text" />
esse método seria chamado no eventochange
.
A instância do componente tem uma API, definida através da seguinte interface:
interface APITabela {
recortarCelulas: () => void;
copiarCelulas: () => void;
colar: () => void;
cancelarAreaTransferencia: () => void;
inserirLinha: () => void;
limparCelulasSelecionadas: () => void;
excluirLinhasSelecionadas: () => void;
}
Método | Descrição |
---|---|
recortarCelulas |
Recorta as células selecionadas. |
copiarCelulas |
Copia as células selecionadas. |
colar |
Cola as células selecionadas ou o conteúdo da área de transferência. |
cancelarAreaTransferencia |
Limpa a seleção de células recortadas/copiadas, e a área de transferência caso existam células recortadas/copiadas. |
inserirLinha |
Insere uma linha em branco na linha da célula atual e move a linha atual e todas as linhas seguintes para baixo. |
limparCelulasSelecionadas |
Limpa o conteúdo das células selecionadas. |
excluirLinhasSelecionadas |
Exclui as linhas selecionadas. |
E como fazer para acessar essa interface? Considere o exemplo abaixo (usando a Composition API):
<script setup lang="ts">
import Tabela, { type APITabela } from '@/components/Tabela.vue';
let colunas: Coluna[] = [
{
Nome: "coluna1",
Descricao: "Coluna 1",
Tipo: "string"
}
];
let linhas: Record<string, any>[] = reactive([
{
coluna1: "Olá, mundo!"
}
]);
let tabela: APITabela;
</script>
<template>
<button @click="tabela.recortarCelulas()">Recortar Células</button>
<button @click="tabela.copiarCelulas()">Copiar Células</button>
<button @click="tabela.colar()">Colar</button>
<button @click="tabela.cancelarAreaTransferencia()">Limpar seleção</button>
<button @click="tabela.inserirLinha()">Inserir linha</button>
<button @click="tabela.limparCelulasSelecionadas()">Limpar conteúdo selecionado</button>
<button @click="tabela.excluirLinhasSelecionadas()">Excluir linhas selecionadas</button>
<Tabela :ref="element => tabela = (element as unknown) as APITabela" />
</template>
É relativamente simples, não tem muito segredo.
O componente utiliza classes para identificar elementos e também indicar seu estado.
- tabela: referencia o elemento da tabela
- editando: colocada no elemento
.tabela
sempre que a tabela está no modo de edição. Também é colocada no elemento.celula
que está sendo editado. - celula: referencia todos os elementos que são células
- cabecalho: referencia todos os elementos
.celula
que fazem parte do cabeçalho da tabela - coluna-1, coluna-2, ..., coluna-N: referencia todos os elementos
.celula
que fazem parte da coluna 1, 2, ..., N da tabela - linha-1, linha-2, ..., linha-N: referencia todos os elementos
.celula
que fazem parte da linha 1, 2, ..., N da tabela - primeira-coluna: referencia todos os elementos da primeira coluna da tabela
- ultima-coluna: referencia todos os elementos da última coluna da tabela
- primeira-linha: referencia todos os elementos da primeira linha da tabela
- ultima-linha: referencia todos os elementos da última linha da tabela
- linha-impar: referencia todas as linhas de dados ímpares da tabela
- linha-par: referencia todas as linhas de dados pares da tabela
- celula-atual: referencia o elemento que representa a célula atual. Também recebe as classes
coluna-1
,coluna-2
, ...,coluna-N
elinha-1
,linha-2
, ...,linha-N
de acordo com o endereço da célula atual. - selecao: referencia o elemento que representa a área das células selecionadas
- area-transferencia: referencia o elemento que representa a área das células copiadas ou recortadas
- inicio-primeira-coluna: colocada nos elementos
.selecao
e.area-transferencia
quando começam na primeira coluna - inicio-ultima-coluna: colocada nos elementos
.selecao
e.area-transferencia
quando começam na última coluna - inicio-primeira-linha: colocada nos elementos
.selecao
e.area-transferencia
quando começam na primeira linha - inicio-ultima-linha: colocada nos elementos
.selecao
e.area-transferencia
quando começam na última linha - fim-primeira-coluna: colocada nos elementos
.selecao
e.area-transferencia
quando terminam na primeira coluna - fim-ultima-coluna: colocada nos elementos
.selecao
e.area-transferencia
quando terminam na última coluna - fim-primeira-linha: colocada nos elementos
.selecao
e.area-transferencia
quando terminam na primeira linha - fim-ultima-linha: colocada nos elementos
.selecao
e.area-transferencia
quando terminam na última linha