Island Details as Expansion Panels

Table, Bro renders details using a layout inspired by Expansion Panels of Google Material Design.
Take control of the cursor, clicking on the button or the table
Loading
Items per page:
import type {BroConfig} from '@table-bro/core';

const config: BroConfig = {
  rowDetailsIsland: true,
  rowDetailsComponent: 'bro-cell-details' // your Details-component as a Custom Element
};

Setup of Island Details

Island Details is a paid module of Table, Bro: the license of Island Details, in Buy on the Home page, gives the registry access token of the module.

  1. Connect the registry of Table, Bro

    In .npmrc of the project; the registry access token of the license comes from the environment variable TABLE_BRO_EXTENDED_TOKEN:

    @table-bro-extended:registry=https://npm.table-bro.com/
    //npm.table-bro.com/:_authToken=${TABLE_BRO_EXTENDED_TOKEN}
    
  2. Install Island Details

    npm install @table-bro-extended/island-details
    
  3. Import the module

    Once, before the tables, in main.ts: it connects itself to every table with rowDetailsIsland.

    import '@table-bro-extended/island-details';
    
  4. Add its CSS

    After the CSS of the skin, in "styles" of angular.json:

    "node_modules/@table-bro-extended/island-details/dist/table-bro.island.css"
    

Keyboard

Enter and Space on the details button open and close Island Details of its row, and the focus stays on the button. The keys of the whole table are on the page Keyboard and focus.

Island Details with server-side data loading

With default indeterminate progress bar:

Take control of the cursor, clicking on the button or the table
Loading
Items per page:
import type {BroConfig} from '@table-bro/core';

const config: BroConfig = {
  rowDetailsIsland: true,
  rowDetailsComponent: 'bro-cell-details', // your Details-component as a Custom Element

  rowDetailsServer: true
};

Island Details with server-side data loading

With determinate progress bar:

Take control of the cursor, clicking on the button or the table
Loading
Items per page:
import type {BroConfig} from '@table-bro/core';

const config: BroConfig = {
  rowDetailsIsland: true,
  rowDetailsComponent: 'bro-cell-details', // your Details-component as a Custom Element

  rowDetailsServer: true,
  rowDetailsProgressMode: 'determinate'
};

Example how to emit progress status from your Details component (e.g. based on your server logic):

import {type BroRow, type BroRowDetailsElement, events} from '@table-bro/core';

interface Order extends BroRow {
  description: string;
  price: number;
}

export class BroCellDetailsComponent extends HTMLElement implements BroRowDetailsElement<Order> {
  // The table sets the row before it connects the element, and the attribute server with rowDetailsServer
  row?: Order;

  private progress: number = 0;
  // The table moves loaded server details from its aux row into the row of the details: the content renders once
  private rendered: boolean = false;
  private timerId?: ReturnType<typeof setInterval>;

  connectedCallback(): void {
    if (this.rendered) {
      return;
    }

    if (this.getAttribute('server') !== 'true') {
      this.render();

      return;
    }

    // Emulates the loading of the details: your server logic reports its progress from 0 to 1
    this.timerId = setInterval(() => {
      this.progress = Math.min(1, this.progress + 0.2);

      if (this.progress === 1) {
        clearInterval(this.timerId);
        this.render();
      }

      this.emitProgress(this.progress);
    }, 400);
  }

  disconnectedCallback(): void {
    clearInterval(this.timerId);
  }

  private emitProgress(progress: number): void {
    this.dispatchEvent(new CustomEvent(events.detailsServerLoadedEvent, {detail: progress}));
  }

  private render(): void {
    this.textContent = `Some details here: ${this.row?.description}`;
    this.rendered = true;
  }
}

// Somewhere in your app, before the table:
customElements.define('bro-cell-details', BroCellDetailsComponent);

or in case of Details is an Angular-component (registered in your Angular-app as a Custom Element):

import {ChangeDetectionStrategy, Component, EventEmitter, inject, Injector, Input, type OnDestroy, type OnInit, Output, signal} from '@angular/core';
import {createCustomElement}                                                                                                    from '@angular/elements';

import {TableBroNg}                                                                  from '@table-bro/angular';
import {type BroCol, type BroConfig, type BroRow, type BroRowDetailsElement, events} from '@table-bro/core';

interface Order extends BroRow {
  description: string;
  price: number;
}

@Component({
  selector: 'bro-cell-order-details',
  template: `
    @if (loaded()) {
      <div>Some details here: {{ row.description }}</div>
    }`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BroCellOrderDetailsComponent implements OnInit, OnDestroy, BroRowDetailsElement<Order> {
  @Input() row!: Order;
  @Input() server?: string;

  // The Custom Element of the component dispatches the output as the event of its name
  @Output(events.detailsServerLoadedEvent) progressChange = new EventEmitter<number>();

  loaded = signal(false);

  private progress: number = 0;
  private timerId?: ReturnType<typeof setInterval>;

  ngOnInit() {
    if (this.server !== 'true') {
      this.loaded.set(true);

      return;
    }

    // Emulates the loading of the details: your server logic reports its progress from 0 to 1
    this.timerId = setInterval(() => {
      this.progress = Math.min(1, this.progress + 0.2);

      if (this.progress < 1) {
        this.progressChange.emit(this.progress);

        return;
      }

      clearInterval(this.timerId);
      this.loaded.set(true);

      // The next turn of the event loop: the details render their content before the table measures them
      setTimeout(() => this.progressChange.emit(1));
    }, 400);
  }

  ngOnDestroy() {
    clearInterval(this.timerId);
  }
}

@Component({
  selector: 'app-orders',
  template: `
    <table-bro-ng [cols]="cols()" [rows]="rows()" [conf]="conf()"></table-bro-ng>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    TableBroNg
  ]
})
export class OrdersComponent {
  cols = signal<BroCol<Order>[]>([
    {
      title: 'Description',
      name: 'description'
    }
  ]);

  rows = signal<Order[] | undefined>(undefined);

  conf = signal<BroConfig>({
    skin: 'mat',
    rowDetailsComponent: 'bro-cell-details', // your Details-component as a Custom Element

    rowDetailsServer: true,
    rowDetailsProgressMode: 'determinate'
  });

  constructor() {
    // The table creates the details by their tag: the Custom Element is defined before it
    if (!customElements.get('bro-cell-details')) {
      customElements.define('bro-cell-details', createCustomElement(BroCellOrderDetailsComponent, {injector: inject(Injector)}));
    }
  }
}
DJ