Criação Dinâmica de Tabelas com JavaScript Orientado a Objetos

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.

Tags: javascript dom-manipulation object-oriented-programming html-tables frontend-development

Publicado em 10-20 00:18