{{ t.pages.datatable.overviewIntro }}

{{ t.common.sectionDescription }}

{{ t.pages.datatable.descriptionBody }}

{{ t.common.sectionFeatures }}
  • {{ t.pages.datatable.featureOne }}
  • {{ t.pages.datatable.featureTwo }}
  • {{ t.pages.datatable.featureThree }}
  • {{ t.pages.datatable.featureFour }}
  • {{ t.pages.datatable.featureFive }}
{{ t.common.sectionLimitations }}
  • {{ t.pages.datatable.limitOne }}
  • {{ t.pages.datatable.limitTwo }}
  • {{ t.pages.datatable.limitThree }}
<li-datatable
  [dataTableFilter]="filters"
  [data]="tableData"
  [settings]="tableSettings"
  [searchInFields]="searchFields"
  [responsiveCollapse]="true"
  (dataRequest)="onTableRequest($event)">
</li-datatable>

{{ t.pages.datatable.demoIntro }}

{{ datatableEventLog.isEmpty ? initialEventLog : datatableEventLog }}
{{ t.pages.datatable.onDemandTitle }}

{{ t.pages.datatable.onDemandIntro }}

{{ t.pages.datatable.lazyModalTitle }}

{{ t.pages.datatable.lazyModalIntro }}

{{ t.pages.datatable.modalBody }}

{{ t.common.sectionHowToUse }}

{{ t.pages.datatable.howToUseBody }}

{{ t.common.sectionMainOptions }}
  • {{ t.pages.datatable.optionOne }}
  • {{ t.pages.datatable.optionTwo }}
  • {{ t.pages.datatable.optionThree }}
  • {{ t.pages.datatable.optionFour }}
{{ t.common.sectionBestPractices }}
  • {{ t.pages.datatable.practiceOne }}
  • {{ t.pages.datatable.practiceTwo }}
  • {{ t.pages.datatable.practiceThree }}
{{ t.pages.datatable.columnStylesTitle }}
  • {{ t.pages.datatable.columnStyleOne }}
  • {{ t.pages.datatable.columnStyleTwo }}
  • {{ t.pages.datatable.columnStyleThree }}
  • {{ t.pages.datatable.columnStyleFour }}
  • {{ t.pages.datatable.columnStyleFive }}
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>