layout.title

layout.subtitle

@ng-orbit/table

Table

A headless table controller for Angular apps that want consumer-owned data fetching, selection, sorting intent, and markup.

Use the directive as the stable contract, then plug in a renderer or your own templates without moving business logic into the visual layer.

Install @ng-orbit/table

    pnpm add @ng-orbit/table @ng-orbit/table-render-plain
  

Import

    import { OrbitTableDirective, type OrbitTableQuery } from '@ng-orbit/table';
import { OrbitTableRenderPlainComponent } from '@ng-orbit/table-render-plain';
  

Mental model

OrbitTable manages query intent and selection state. The parent still owns the data source, server calls, and row updates.

  • No HTML table markup is shipped by the core package.
  • No in-memory sorting, filtering, or pagination rules are enforced.
  • Renderers stay thin because they only call controller commands.

What the consumer owns

Your feature layer remains in charge of data fetching, query persistence, analytics, and empty or error states.

  • Translate OrbitTableQuery into backend params.
  • Provide rows, total, loading, and error inputs.
  • Choose between renderer packages or custom markup.

What the package does not do

The table core is intentionally conservative so teams can plug it into very different design systems.

  • No data adapter layer or HTTP client opinion.
  • No CSS, tokens, or design-system dependency.
  • No business filtering or row action logic.

Quickstart

    import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  OrbitTableDirective,
  createDefaultOrbitTableQuery,
  type OrbitTableColumn,
  type OrbitTableQuery
} from '@ng-orbit/table';
import { OrbitTableRenderPlainComponent } from '@ng-orbit/table-render-plain';

interface UserRow {
  id: number;
  fullName: string;
  email: string;
}

@Component({
  standalone: true,
  imports: [OrbitTableDirective, OrbitTableRenderPlainComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <section
      orbitTable
      #table="orbitTable"
      [rows]="rows()"
      [columns]="columns"
      [total]="total()"
      [loading]="loading()"
      [error]="error()"
      [query]="query()"
      [getRowId]="getRowId"
      (orbitTableQueryChange)="onQueryChange($event)"
    >
      <orbit-table-render-plain [table]="table" />
    </section>
  `
})
export class UsersTableComponent {
  readonly query = signal(createDefaultOrbitTableQuery());
  readonly rows = signal<readonly UserRow[]>([]);
  readonly total = signal(0);
  readonly loading = signal(false);
  readonly error = signal<unknown | null>(null);

  readonly columns: readonly OrbitTableColumn<UserRow>[] = [
    { id: 'fullName', header: 'Name', accessor: (row) => row.fullName, sortable: true },
    { id: 'email', header: 'Email', accessor: (row) => row.email, sortable: true }
  ];

  readonly getRowId = (row: UserRow) => row.id;

  onQueryChange(query: OrbitTableQuery): void {
    this.query.set(query);
    this.fetchUsers(query);
  }

  private fetchUsers(query: OrbitTableQuery): void {
    // Fetch data using your backend contract, then update rows/total/loading/error.
  }
}