Este artigo apresenta uma abordagem robusta para gerar tabelas HTML dinamicamente usando JavaScript moderno, com foco em encapsulamento, reatividade e manutenção limpa. Ao contrário de soluções baseadas em manipulação direta do DOM ou concatenação de strings, adotamos um padrão orientado a objetos que separa claramente a estrutura de dados, a renderização visual e o comportamento interativo.
Estrutura Base da Classe
Iniciamos com uma classe auxiliar DomHelper, responsável por operações comuns de manipulação do DOM — criação de elementos e seleção de nós — sem depender de bibliotecas externas:
class DomHelper {
createElement(tag) {
return document.createElement(tag);
}
query(selector) {
return document.querySelector(selector) ?? document.getElementById(selector);
}
}
Classe Principal: DynamicTable
A classe principal herda de DomHelper e recebe três parâmetros no construtor: um seletor CSS para o contêiner de destino, um array de objetos de dados e um booleano opcional para habilitar coluna de ações:
class DynamicTable extends DomHelper {
constructor(containerSelector, dataset, enableActions = false) {
super();
this.container = this.query(containerSelector);
this.data = Array.isArray(dataset) ? [...dataset] : [];
this.enableActions = enableActions;
this.actionButtons = [];
this.render();
}
// Gera toda a estrutura da tabela
buildStructure() {
const table = this.createElement('table');
const thead = this.createElement('thead');
const tbody = this.createElement('tbody');
// Cabeçalho: gera <th> com base nas chaves do primeiro item
if (this.data.length > 0 && typeof this.data[0] === 'object') {
const headerRow = this.createElement('tr');
const keys = Object.keys(this.data[0]);
keys.forEach(key => {
const th = this.createElement('th');
th.textContent = key;
headerRow.appendChild(th);
});
if (this.enableActions) {
const actionTh = this.createElement('th');
actionTh.textContent = 'Ações';
headerRow.appendChild(actionTh);
}
thead.appendChild(headerRow);
}
// Corpo: itera sobre cada item e gera linhas
this.data.forEach((item, index) => {
const row = this.createElement('tr');
Object.values(item).forEach(value => {
const cell = this.createElement('td');
cell.textContent = value;
row.appendChild(cell);
});
if (this.enableActions) {
const actionCell = this.createElement('td');
const deleteBtn = this.createElement('button');
deleteBtn.textContent = 'Excluir';
deleteBtn.dataset.index = index;
actionCell.appendChild(deleteBtn);
row.appendChild(actionCell);
this.actionButtons.push(deleteBtn);
}
tbody.appendChild(row);
});
table.appendChild(thead);
table.appendChild(tbody);
return table;
}
// Renderiza a tabela no DOM, removendo versões anteriores
render() {
if (this.container.firstChild) {
this.container.innerHTML = '';
}
const tableElement = this.buildStructure();
this.container.appendChild(tableElement);
this.bindActionEvents();
}
// Vincula eventos de clique aos botões de ação
bindActionEvents() {
this.actionButtons.forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.index, 10);
if (!isNaN(idx) && idx >= 0 && idx < this.data.length) {
this.data.splice(idx, 1);
this.render();
}
});
});
}
// Adiciona novo registro e atualiza a tabela
addRecord(record) {
if (record && typeof record === 'object') {
this.data.push({ ...record });
this.render();
}
}
// Remove registro pelo índice
removeRecord(index) {
if (index >= 0 && index < this.data.length) {
this.data.splice(index, 1);
this.render();
}
}
}
Uso Prático no HTML
No arquivo HTML, basta incluir um contêiner e os campos de entrada para inserção de novos registros:
<div class="table-container"></div>
<fieldset>
<legend>Adicionar Novo Registro</legend>
<input type="text" id="input-name" placeholder="Nome">
<input type="number" id="input-age" placeholder="Idade">
<input type="text" id="input-hobby" placeholder="Hobby">
<button id="btn-add">Inserir</button>
</fieldset>
Integração com Interface de Usuário
O script de inicialização configura a tabela com dados iniciais e vincula o botão de inserção:
const initialData = [
{ id: 'a1b2', nome: 'Ana Silva', idade: 28, hobby: 'Leitura' },
{ id: 'c3d4', nome: 'Bruno Costa', idade: 35, hobby: 'Ciclismo' }
];
const tabela = new DynamicTable('.table-container', initialData, true);
document.getElementById('btn-add').addEventListener('click', () => {
const nameInput = document.getElementById('input-name');
const ageInput = document.getElementById('input-age');
const hobbyInput = document.getElementById('input-hobby');
const nome = nameInput.value.trim();
const idade = parseInt(ageInput.value, 10);
const hobby = hobbyInput.value.trim();
if (nome && !isNaN(idade) && hobby) {
tabela.addRecord({
id: Math.random().toString(36).substr(2, 9),
nome,
idade,
hobby
});
// Limpa os campos após inserção
nameInput.value = '';
ageInput.value = '';
hobbyInput.value = '';
} else {
alert('Preencha todos os campos corretamente.');
}
});
Estilização Básica
Para garantir legibilidade e aparência consistente, aplicamos estilos mínimos:
table {
width: 100%;
max-width: 600px;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
padding: 0.75rem;
text-align: left;
border: 1px solid #ccc;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
button {
background-color: #e74c3c;
color: white;
border: none;
padding: 0.4rem 0.8rem;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #c0392b;
}
Vantagens Arquiteturais
- Encapsulamento claro: cada responsabilidade (DOM, dados, eventos) está isolada em métodos distintos.
- Não depende de mutação global: não altera protótipos nativos como
Array.prototype, evitando efeitos colaterais indesejados. - Reutilizável: pode ser instenciada múltiplas vezes com diferentes conjuntos de dados e contêineres.
- Fácil de testar: métodos puramente lógicos podem ser validados sem dependência do DOM.
- Extensível: suporta fácil adição de funcionalidades como ordenação, paginação ou edição inline.