{{ t.pages.datatable.cardTitle }}

A datatable desta biblioteca cobre o fluxo administrativo mais comum: busca por campo, seleção de linhas, exportação, paginação, ordenação, colapso responsivo e uma camada de customização visual por coluna, por linha e por card.

Descrição

O componente atende listagens operacionais que precisam alternar entre tabela e grade sem duplicar fonte de dados, regras de ordenação ou comportamento de busca.

Recursos
  • Busca direcionada por coluna com campo e operador configuráveis.
  • Exportação para XLSX e PDF usando a própria API do componente.
  • Modo tabela e modo grade com a mesma fonte de dados.
  • Largura, alinhamento, classes e estilos por coluna e por linha.
  • Cards customizados com customCardBuilder em grid mode.
Limitações
  • cellStyleResolver trabalha com CSS inline, então não substitui um tema completo.
  • customCardBuilder é ideal para cards ricos, mas exige montagem manual do markup.
  • O colapso mobile continua orientado a colunas, então layouts muito densos pedem curadoria das colunas secundárias.
<li-datatable
  [dataTableFilter]="filters"
  [data]="tableData"
  [settings]="tableSettings"
  [searchInFields]="searchFields"
  [responsiveCollapse]="true"
  (dataRequest)="onTableRequest($event)">
</li-datatable>

Demo principal com busca, seleção, exportação e alternância entre tabela e grade.

{{ datatableEventLog.isEmpty ? initialEventLog : datatableEventLog }}
Demos sob demanda

Os exemplos abaixo usam li-accordion com carregamento tardio para evitar que todos os datatables entrem no DOM ao abrir a rota.

Modal lazy loading com datatable

Este cenário abre um li-modal com lazyContent e instancia o li-datatable somente depois da abertura.

O conteúdo do modal só entra no DOM quando ele abre. O datatable reutiliza o mesmo dataset da página para verificar se o primeiro render funciona corretamente.

Como utilizar

O componente é controlado por três peças: Filters para paginação e busca, DataFrame para os dados e DatatableSettings para as colunas e o comportamento visual.

Opções principais
  • [dataTableFilter]: controla limite, offset, busca e ordenação.
  • [settings]: define colunas, grid e builders customizados.
  • [searchInFields]: informa quais campos aparecem no seletor de busca.
  • [responsiveCollapse]: move colunas secundárias para a linha filha no mobile.
Boas práticas
  • Mantenha o DataFrame estável e atualize apenas filtros e seleção.
  • Use hideOnMobile nas colunas secundárias.
  • Reserve customCardBuilder para grids que realmente precisam fugir do layout padrão.
Colunas e estilos
  • width, minWidth e maxWidth controlam a largura efetiva da coluna.
  • headerClass e cellClass adicionam classes sem mexer no template.
  • styleCss e cellStyleResolver controlam cor e estilo por coluna.
  • rowStyleResolver devolve uma string CSS por linha inteira com base nos dados.
  • textAlign e nowrap ajustam leitura para colunas curtas ou status.
DatatableCol(
  key: 'status',
  title: 'Status',
  width: '160px',
  textAlign: 'center',
  nowrap: true,
  cellStyleResolver: (itemMap, itemInstance) {
    final status = itemMap['status']?.toString() ?? '';
    return status == 'Bloqueado'
        ? 'color: #b91c1c; font-weight: 700;'
        : 'color: #0f766e; font-weight: 700;';
  },
)
RowStyleResolver e grid
  • rowStyleResolver permite destacar uma linha inteira sem alterar o template.
  • gridTemplateColumns define a malha do grid.
  • gridGap controla o espaçamento entre cartões.
  • customCardBuilder recebe itemMap, itemInstance e row para montar um Element completo.
DatatableSettings(
  colsDefinitions: cols,
  rowStyleResolver: (itemMap, itemInstance) {
    if (itemMap['health'] == 'Crítica') {
      return 'background-color: rgba(239, 68, 68, 0.08);';
    }
    return null;
  },
  gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
  gridGap: '1rem',
  customCardBuilder: (itemMap, itemInstance, row) {
    final root = DivElement()..classes.add('my-card');
    root.text = itemMap['feature']?.toString() ?? '';
    return root;
  },
)
Fluxo mínimo full stack com Product

Abaixo está um fluxo mínimo, baseado no padrão real de backend e frontend deste repositório. Os snippets ficam estáticos no próprio template para evitar sobrecarga dinâmica.

1. Model Product

Estrutura mínima seguindo o padrão de models do core com SerializeBase.

import 'serialize_base.dart';

class Product implements SerializeBase {
  static const tableName = 'products';
  static const fqtn = 'public.$tableName';
  static const idCol = 'id';
  static const nameCol = 'name';
  static const priceCol = 'price';
  static const statusCol = 'status';

  int id;
  String name;
  double price;
  String status;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.status,
  });

  @override
  Map<String, dynamic> toMap() {
    return {
      idCol: id,
      nameCol: name,
      priceCol: price,
      statusCol: status,
    };
  }

  Map<String, dynamic> toInsertMap() => toMap()..remove(idCol);

  Map<String, dynamic> toUpdateMap() => toMap()..remove(idCol);

  factory Product.fromMap(Map<String, dynamic> map) {
    return Product(
      id: map[idCol] as int,
      name: map[nameCol] as String,
      price: (map[priceCol] as num).toDouble(),
      status: map[statusCol] as String,
    );
  }
}
2. Backend com Eloquent + DataFrame

O repositório consulta a tabela, aplica filtros, paginação e ordenação, e retorna um DataFrame pronto para serialização.

class ProductRepository {
  final Connection db;

  ProductRepository(this.db);

  Future<DataFrame<Map<String, dynamic>>> list({Filters? filtros}) async {
    final query = db.table(Product.fqtn);
    query.selectRaw('*');

    if (filtros?.isSearch == true) {
      final search = '%${filtros!.searchString!.toLowerCase()}%';
      query.whereRaw('unaccent(name) ilike unaccent(?)', [search]);
    }

    final totalRecords = await query.count();

    if (filtros?.isOrder == true) {
      query.orderBy(filtros!.orderBy!, filtros.orderDir!);
    } else {
      query.orderBy(Product.nameCol, 'asc');
    }

    if (filtros?.isLimit == true) {
      query.limit(filtros!.limit!);
    }
    if (filtros?.isOffset == true) {
      query.offset(filtros!.offset!);
    }

    final rows = await query.get();
    return DataFrame<Map<String, dynamic>>(
      items: rows,
      totalRecords: totalRecords,
    );
  }
}

class ProductController {
  static Future<Response> list(Request req) async {
    final filtros = Filters.fromMap(req.url.queryParameters);
    final repo = req.make<ProductRepository>();
    final data = await repo.list(filtros: filtros);
    return responseDataFrame(data);
  }
}
3. Service no frontend

O service consome a rota e transforma o JSON em DataFrame<Product> usando o builder.

class ProductService extends RestServiceBase {
  ProductService(RestConfig conf) : super(conf);

  final String path = '/products';

  Future<DataFrame<Product>> list(Filters filtros) async {
    return getDataFrame<Product>(
      path,
      builder: Product.fromMap,
      filtros: filtros,
    );
  }
}
4. Page AngularDart

A page mantém Filters, DatatableSettings e chama o service sempre que o datatable emite dataRequest.

class ListaProdutoPage implements OnActivate {
  ListaProdutoPage(this.hostElement, this.productService);

  final Element hostElement;
  final ProductService productService;

  final filtro = Filters(limit: 12, offset: 0);
  DataFrame<Product> items = DataFrame<Product>.newClear();

  final DatatableSettings dtSettings = DatatableSettings(
    colsDefinitions: [
      DatatableCol(key: 'id', title: 'Id', sortingBy: 'id', enableSorting: true),
      DatatableCol(key: 'name', title: 'Nome', sortingBy: 'name', enableSorting: true),
      DatatableCol(key: 'price', title: 'Preço'),
      DatatableCol(key: 'status', title: 'Status', hideOnMobile: true),
    ],
  );

  final List<DatatableSearchField> sInFields = <DatatableSearchField>[
    DatatableSearchField(field: 'name', operator: 'like', label: 'Nome'),
    DatatableSearchField(field: 'status', operator: '=', label: 'Status'),
  ];

  Future<void> load() async {
    final loading = SimpleLoading();
    try {
      loading.show(target: hostElement);
      items = await productService.list(filtro);
    } finally {
      loading.hide();
    }
  }
}
5. Template com li-datatable

No template, o datatable recebe o filtro, os dados e as definições de coluna, reproduzindo o padrão das páginas reais do frontend.

<div class="card">
  <li-datatable
      [dataTableFilter]="filtro"
      [data]="items"
      [settings]="dtSettings"
      [searchInFields]="sInFields"
      (dataRequest)="onDtRequestData($event)">
  </li-datatable>
</div>