{{ 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
customCardBuilderem grid mode.
Limitações
cellStyleResolvertrabalha 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.
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
DataFrameestável e atualize apenas filtros e seleção. - Use
hideOnMobilenas colunas secundárias. - Reserve
customCardBuilderpara grids que realmente precisam fugir do layout padrão.
Colunas e estilos
width,minWidthemaxWidthcontrolam a largura efetiva da coluna.headerClassecellClassadicionam classes sem mexer no template.styleCssecellStyleResolvercontrolam cor e estilo por coluna.rowStyleResolverdevolve uma string CSS por linha inteira com base nos dados.textAlignenowrapajustam 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
rowStyleResolverpermite destacar uma linha inteira sem alterar o template.gridTemplateColumnsdefine a malha do grid.gridGapcontrola o espaçamento entre cartões.customCardBuilderrecebeitemMap,itemInstanceerowpara montar umElementcompleto.
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>