npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@toolbox-web/grid-angular

v0.14.2

Published

Angular adapter for @toolbox-web/grid data grid component

Readme

@toolbox-web/grid-angular

npm License: MIT GitHub Sponsors

Angular adapter for @toolbox-web/grid data grid component. Provides directives for declarative template-driven cell renderers and editors.

Features

  • Auto-adapter registration - Just import Grid directive
  • Structural directives - Clean *tbwRenderer and *tbwEditor syntax
  • Template-driven renderers - Use <ng-template> for custom cell views
  • Template-driven editors - Use <ng-template> for custom cell editors
  • Component-class column config - Specify component classes directly in gridConfig.columns
  • Type-level defaults - App-wide renderers/editors via provideGridTypeDefaults()
  • Icon configuration - App-wide icon overrides via provideGridIcons()
  • Reactive Forms integration - Use formControlName and formControl bindings
  • Auto-wiring - Editor components just emit events, no manual binding needed
  • Full type safety - Typed template contexts (GridCellContext, GridEditorContext)
  • Angular 17+ - Standalone components, signals support
  • AOT compatible - Works with Angular's ahead-of-time compilation

Installation

# npm
npm install @toolbox-web/grid @toolbox-web/grid-angular

# yarn
yarn add @toolbox-web/grid @toolbox-web/grid-angular

# pnpm
pnpm add @toolbox-web/grid @toolbox-web/grid-angular

# bun
bun add @toolbox-web/grid @toolbox-web/grid-angular

Quick Start

1. Register the Grid Component

In your Angular application, import the grid registration:

// main.ts or app.config.ts
import '@toolbox-web/grid';

2. Use in Components

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';

@Component({
  selector: 'app-my-grid',
  imports: [Grid],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: ` <tbw-grid [rows]="rows" [gridConfig]="config" style="height: 400px; display: block;"> </tbw-grid> `,
})
export class MyGridComponent {
  rows = [
    { id: 1, name: 'Alice', status: 'active' },
    { id: 2, name: 'Bob', status: 'inactive' },
  ];

  config = {
    columns: [
      { field: 'id', header: 'ID', type: 'number' },
      { field: 'name', header: 'Name' },
      { field: 'status', header: 'Status' },
    ],
  };
}

Structural Directives (Recommended)

The cleanest way to define custom renderers and editors is with structural directives. These provide a concise syntax without the boilerplate of nested <ng-template> elements.

TbwRenderer

Use *tbwRenderer to customize how cell values are displayed:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Grid, TbwRenderer } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, TbwRenderer, StatusBadgeComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <tbw-grid [rows]="rows" [gridConfig]="config">
      <tbw-grid-column field="status">
        <app-status-badge *tbwRenderer="let value" [value]="value" />
      </tbw-grid-column>
    </tbw-grid>
  `,
})
export class MyGridComponent {}

Template Context:

| Variable | Type | Description | | ----------- | --------- | ------------------------------------- | | $implicit | TValue | The cell value (use with let-value) | | row | TRow | The full row data object | | column | unknown | The column configuration |

TbwEditor

Use *tbwEditor for custom cell editors. The adapter automatically listens for commit and cancel events from your component, so you don't need to manually wire up callbacks:

import { Component, CUSTOM_ELEMENTS_SCHEMA, output } from '@angular/core';
import { Grid, TbwRenderer, TbwEditor } from '@toolbox-web/grid-angular';

// Your editor component just needs to emit 'commit' and 'cancel' events
@Component({
  selector: 'app-status-editor',
  template: `
    <select [value]="value()" (change)="commit.emit($any($event.target).value)">
      <option value="active">Active</option>
      <option value="inactive">Inactive</option>
    </select>
  `,
})
export class StatusEditorComponent {
  value = input<string>();
  commit = output<string>();
  cancel = output<void>();
}

@Component({
  imports: [Grid, TbwRenderer, TbwEditor, StatusBadgeComponent, StatusEditorComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <tbw-grid [rows]="rows" [gridConfig]="config">
      <tbw-grid-column field="status" editable>
        <app-status-badge *tbwRenderer="let value" [value]="value" />
        <app-status-editor *tbwEditor="let value" [value]="value" />
      </tbw-grid-column>
    </tbw-grid>
  `,
})
export class MyGridComponent {}

Template Context:

| Variable | Type | Description | | ----------- | ----------------- | ------------------------------------------------------ | | $implicit | TValue | The cell value (use with let-value) | | row | TRow | The full row data object | | column | unknown | The column configuration | | onCommit | Function | Callback to commit (optional with auto-wire) | | onCancel | Function | Callback to cancel (optional with auto-wire) | | control | AbstractControl | FormControl for cell (when using FormArray+FormGroups) |

Auto-wiring: If your editor component emits a commit event with the new value, the adapter automatically calls the grid's commit function. Similarly for cancel. This means you can skip the explicit onCommit/onCancel bindings!

Nested Directive Syntax (Alternative)

For more explicit control, you can use the nested directive syntax with <ng-template>:

GridColumnView

import { Grid, GridColumnView } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridColumnView],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <tbw-grid [rows]="rows" [gridConfig]="config">
      <tbw-grid-column field="status">
        <tbw-grid-column-view>
          <ng-template let-value let-row="row">
            <span [class]="'badge badge--' + value">{{ value }}</span>
          </ng-template>
        </tbw-grid-column-view>
      </tbw-grid-column>
    </tbw-grid>
  `
})

GridColumnEditor

import { Grid, GridColumnView, GridColumnEditor } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridColumnView, GridColumnEditor],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <tbw-grid [rows]="rows" [gridConfig]="config">
      <tbw-grid-column field="status" editable>
        <tbw-grid-column-view>
          <ng-template let-value>
            <span [class]="'badge badge--' + value">{{ value }}</span>
          </ng-template>
        </tbw-grid-column-view>
        <tbw-grid-column-editor>
          <ng-template let-value let-commit="commit" let-cancel="cancel">
            <select [value]="value" (change)="commit.emit($any($event.target).value)">
              <option value="active">Active</option>
              <option value="inactive">Inactive</option>
            </select>
          </ng-template>
        </tbw-grid-column-editor>
      </tbw-grid-column>
    </tbw-grid>
  `
})

Grid-Level Events

The Grid directive provides convenient outputs for common grid events:

import { Grid, CellCommitEvent, RowCommitEvent } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid],
  template: `
    <tbw-grid
      [rows]="rows"
      [gridConfig]="config"
      (cellCommit)="onCellCommit($event)"
      (rowCommit)="onRowCommit($event)"
    />
  `,
})
export class MyGridComponent {
  onCellCommit(event: CellCommitEvent<Employee>) {
    console.log('Cell edited:', event.field, event.oldValue, '→', event.newValue);
  }

  onRowCommit(event: RowCommitEvent<Employee>) {
    console.log('Row saved:', event.rowIndex, event.row);
  }
}

Master-Detail Panels

Use GridDetailView for expandable row details:

import { Grid, GridDetailView } from '@toolbox-web/grid-angular';
import { MasterDetailPlugin } from '@toolbox-web/grid/all';

@Component({
  imports: [Grid, GridDetailView, DetailPanelComponent],
  template: `
    <tbw-grid [rows]="rows" [gridConfig]="config">
      <tbw-grid-detail showExpandColumn animation="slide">
        <ng-template let-row>
          <app-detail-panel [employee]="row" />
        </ng-template>
      </tbw-grid-detail>
    </tbw-grid>
  `,
})
export class MyGridComponent {
  config = {
    plugins: [new MasterDetailPlugin()],
    // ... columns
  };
}

Custom Tool Panels

Add custom sidebar panels with GridToolPanel:

import { Grid, GridToolPanel } from '@toolbox-web/grid-angular';

@Component({
  imports: [Grid, GridToolPanel, QuickFiltersPanelComponent],
  template: `
    <tbw-grid [rows]="rows" [gridConfig]="config">
      <tbw-grid-tool-panel
        id="filters"
        title="Quick Filters"
        icon="🔍"
        tooltip="Filter the data"
        [order]="10"
      >
        <ng-template let-grid>
          <app-quick-filters [grid]="grid" />
        </ng-template>
      </tbw-grid-tool-panel>
    </tbw-grid>
  `,
})

Type-Level Defaults

Define app-wide renderers and editors for custom column types using provideGridTypeDefaults():

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideGridTypeDefaults } from '@toolbox-web/grid-angular';
import { CountryBadgeComponent, CountryEditorComponent, CurrencyCellComponent } from './components';

export const appConfig: ApplicationConfig = {
  providers: [
    provideGridTypeDefaults({
      country: {
        renderer: CountryBadgeComponent,
        editor: CountryEditorComponent,
      },
      currency: {
        renderer: CurrencyCellComponent,
      },
    }),
  ],
};

Then any grid with columns using type: 'country' will automatically use the registered components:

// my-grid.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
import type { GridConfig } from '@toolbox-web/grid';

@Component({
  imports: [Grid],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<tbw-grid [rows]="data" [gridConfig]="config" />`,
})
export class MyGridComponent {
  config: GridConfig = {
    columns: [
      { field: 'name', header: 'Name' },
      { field: 'country', type: 'country', editable: true }, // Uses registered components
      { field: 'salary', type: 'currency' },
    ],
    plugins: [new EditingPlugin()],
  };
}

Services:

| Service | Description | | -------------------- | ------------------------------------------- | | GridTypeRegistry | Injectable service for dynamic registration | | GRID_TYPE_DEFAULTS | Injection token for type defaults |

App-Wide Icon Configuration

Customize grid icons at the application level using provideGridIcons():

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideGridIcons } from '@toolbox-web/grid-angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideGridIcons({
      expand: '➕',
      collapse: '➖',
      sortAsc: '↑',
      sortDesc: '↓',
      filter: '<svg viewBox="0 0 16 16">...</svg>',
    }),
  ],
};

Dynamic Icon Registration:

import { Component, inject, OnInit } from '@angular/core';
import { GridIconRegistry } from '@toolbox-web/grid-angular';

@Component({ ... })
export class AppComponent implements OnInit {
  private iconRegistry = inject(GridIconRegistry);

  ngOnInit() {
    // Dynamically set icons
    this.iconRegistry.set('expand', '▶');
    this.iconRegistry.set('collapse', '▼');
  }
}

Available Icons:

| Icon | Default | Description | | -------------- | ------- | ------------------------------------ | | expand | | Expand icon for trees/groups/details | | collapse | | Collapse icon | | sortAsc | | Sort ascending indicator | | sortDesc | | Sort descending indicator | | sortNone | | Unsorted indicator | | filter | SVG | Filter icon in headers | | filterActive | SVG | Filter icon when active | | submenuArrow | | Context menu submenu arrow | | dragHandle | ⋮⋮ | Drag handle for reordering | | toolPanel | | Tool panel toggle icon | | print | 🖨️ | Print button icon |

Precedence (highest wins):

  1. gridConfig.icons - Per-grid overrides
  2. provideGridIcons() - App-level defaults
  3. Built-in defaults

Component-Class Column Config

For maximum flexibility and type safety, you can specify Angular component classes directly in your gridConfig.columns. This approach gives you full control over the component lifecycle while keeping your grid configuration clean and concise.

Component Interfaces

Your components should implement one of these interfaces:

Renderer components:

import { Component, input } from '@angular/core';
import type { CellRenderer, ColumnConfig } from '@toolbox-web/grid-angular';

@Component({
  selector: 'app-status-badge',
  template: `<span [class]="'badge badge--' + value()">{{ value() }}</span>`,
  standalone: true,
})
export class StatusBadgeComponent implements CellRenderer<Employee, string> {
  value = input.required<string>();
  row = input.required<Employee>();
  column = input<ColumnConfig>(); // Optional
}

Editor components:

import { Component, input, output } from '@angular/core';
import type { CellEditor, ColumnConfig } from '@toolbox-web/grid-angular';

@Component({
  selector: 'app-bonus-editor',
  template: `
    <input type="range" [min]="0" [max]="maxBonus()" [value]="value()" (input)="onInput($event)" />
    <button (click)="cancel.emit()">Cancel</button>
  `,
  standalone: true,
})
export class BonusEditorComponent implements CellEditor<Employee, number> {
  value = input.required<number>();
  row = input.required<Employee>();
  column = input<ColumnConfig>(); // Optional

  commit = output<number>();
  cancel = output<void>();

  // Computed property using row data
  maxBonus = computed(() => this.row().salary * 0.5);

  onInput(event: Event) {
    const newValue = Number((event.target as HTMLInputElement).value);
    this.commit.emit(newValue);
  }
}

Using Components in Grid Config

Use GridConfig with the gridConfig input for type-safe component references:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Grid, type GridConfig } from '@toolbox-web/grid-angular';
import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
import { StatusBadgeComponent, BonusEditorComponent } from './components';

@Component({
  imports: [Grid],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<tbw-grid [gridConfig]="config" [rows]="employees" />`,
})
export class MyGridComponent {
  config: GridConfig<Employee> = {
    columns: [
      { field: 'name', header: 'Name' },
      { field: 'status', header: 'Status', renderer: StatusBadgeComponent },
      { field: 'bonus', header: 'Bonus', editable: true, editor: BonusEditorComponent },
    ],
    plugins: [new EditingPlugin()],
  };
}

The gridConfig input accepts GridConfig (Angular-augmented) which allows both component classes and vanilla JS functions. When component classes are detected, the directive automatically converts them to grid-compatible renderer functions.

Interfaces Reference

| Interface | Required Inputs | Required Outputs | Description | | -------------- | ------------------ | ------------------ | ------------------ | | CellRenderer | value(), row() | - | Read-only renderer | | CellEditor | value(), row() | commit, cancel | Editable cell |

Both interfaces also support an optional column() input for accessing the column configuration.

Using Plugins

Import plugins individually for smaller bundles:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';
import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';

@Component({
  imports: [Grid],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<tbw-grid [rows]="rows" [gridConfig]="config" />`,
})
export class MyGridComponent {
  config = {
    columns: [...],
    plugins: [
      new SelectionPlugin({ mode: 'row' }),
      new FilteringPlugin({ debounceMs: 200 }),
    ],
  };
}

Or import all plugins at once (larger bundle, but convenient):

import { SelectionPlugin, FilteringPlugin } from '@toolbox-web/grid/all';

Reactive Forms Integration

The grid can be used as an Angular form control with formControlName or formControl bindings. This enables seamless integration with Angular's Reactive Forms system.

Basic Usage with FormControl

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Grid, GridFormArray } from '@toolbox-web/grid-angular';
import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
import type { GridConfig } from '@toolbox-web/grid';

interface Employee {
  name: string;
  age: number;
}

@Component({
  imports: [Grid, GridFormArray, ReactiveFormsModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <tbw-grid [formControl]="employeesControl" [gridConfig]="config" style="height: 400px; display: block;" />

    <div>
      <p>Form value: {{ employeesControl.value | json }}</p>
      <p>Dirty: {{ employeesControl.dirty }}</p>
      <p>Touched: {{ employeesControl.touched }}</p>
    </div>
  `,
})
export class MyComponent {
  employeesControl = new FormControl<Employee[]>([
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 },
  ]);

  config: GridConfig<Employee> = {
    columns: [
      { field: 'name', header: 'Name', editable: true },
      { field: 'age', header: 'Age', editable: true, type: 'number' },
    ],
    plugins: [new EditingPlugin({ editOn: 'dblclick' })],
  };
}

Usage with FormGroup

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Grid, GridFormArray } from '@toolbox-web/grid-angular';
import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';

@Component({
  imports: [Grid, GridFormArray, ReactiveFormsModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <form [formGroup]="form">
      <tbw-grid formControlName="employees" [gridConfig]="config" style="height: 400px; display: block;" />
    </form>
  `,
})
export class MyComponent {
  form = new FormGroup({
    employees: new FormControl([
      { name: 'Alice', age: 30 },
      { name: 'Bob', age: 25 },
    ]),
  });

  config = {
    columns: [
      { field: 'name', header: 'Name', editable: true },
      { field: 'age', header: 'Age', editable: true },
    ],
    plugins: [new EditingPlugin()],
  };
}

How It Works

  • Form → Grid: When the form value changes (e.g., via setValue() or patchValue()), the grid rows are updated
  • Grid → Form: When a cell is edited and committed, the form value is updated with the new row data
  • Touched state: The form becomes touched when the user clicks on the grid
  • Dirty state: The form becomes dirty when any cell is edited
  • Disabled state: When the form control is disabled, the grid adds a .form-disabled CSS class

Validation

You can add validators to validate the entire grid data:

import { Validators } from '@angular/forms';

employeesControl = new FormControl<Employee[]>([], [
  Validators.required, // At least one row
  Validators.minLength(2), // At least 2 rows
  this.validateEmployees, // Custom validator
]);

validateEmployees(control: FormControl<Employee[]>) {
  const employees = control.value || [];
  const hasInvalidAge = employees.some((e) => e.age < 18);
  return hasInvalidAge ? { invalidAge: true } : null;
}

CSS Classes

Angular's form system automatically adds these classes to the grid element:

  • .ng-valid / .ng-invalid - Validation state
  • .ng-pristine / .ng-dirty - Edit state
  • .ng-untouched / .ng-touched - Touch state

Additionally, when the control is disabled:

  • .form-disabled - Added by GridFormArray

You can style these states:

tbw-grid.ng-invalid.ng-touched {
  border: 2px solid red;
}

tbw-grid.form-disabled {
  opacity: 0.6;
  pointer-events: none;
}

Advanced: Cell-Level FormControls with FormArray

For fine-grained control over validation and form state at the cell level, use a FormArray of FormGroups. This approach exposes the FormControl for each cell in the editor context, allowing custom editors to bind directly:

import { Component, CUSTOM_ELEMENTS_SCHEMA, input, output } from '@angular/core';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Grid, GridFormArray, TbwEditor, TbwRenderer } from '@toolbox-web/grid-angular';
import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';

// Custom editor that uses the FormControl directly
@Component({
  selector: 'app-validated-input',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    @if (control()) {
      <input [formControl]="control()" [class.is-invalid]="control()!.invalid && control()!.touched" />
      @if (control()!.invalid && control()!.touched) {
        <small class="error">{{ getErrorMessage() }}</small>
      }
    }
  `,
  styles: `
    .is-invalid {
      border-color: red;
    }
    .error {
      color: red;
      font-size: 0.8em;
    }
  `,
})
export class ValidatedInputComponent {
  control = input<AbstractControl>();
  commit = output<string>();

  getErrorMessage(): string {
    const ctrl = this.control();
    if (ctrl?.hasError('required')) return 'Required';
    if (ctrl?.hasError('min')) return 'Too low';
    return 'Invalid';
  }
}

@Component({
  imports: [Grid, GridFormArray, TbwRenderer, TbwEditor, ReactiveFormsModule, ValidatedInputComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <tbw-grid [formControl]="employeesArray" [gridConfig]="config">
      <tbw-grid-column field="age" editable>
        <span *tbwRenderer="let value">{{ value }}</span>
        <!-- The 'control' property gives you the FormControl for this cell -->
        <app-validated-input *tbwEditor="let value; control as ctrl" [control]="ctrl" />
      </tbw-grid-column>
    </tbw-grid>
  `,
})
export class MyComponent {
  // Use FormArray with FormGroups for cell-level control access
  employeesArray = new FormArray([
    new FormGroup({
      name: new FormControl('Alice', Validators.required),
      age: new FormControl(30, [Validators.required, Validators.min(18)]),
    }),
    new FormGroup({
      name: new FormControl('Bob', Validators.required),
      age: new FormControl(25, [Validators.required, Validators.min(18)]),
    }),
  ]);

  config = {
    columns: [
      { field: 'name', header: 'Name', editable: true },
      { field: 'age', header: 'Age', editable: true },
    ],
    plugins: [new EditingPlugin()],
  };
}

Editor Context with FormControl:

| Variable | Type | Description | | ---------- | ----------------- | ------------------------------------------------------------------ | | value | TValue | The cell value | | row | TRow | The full row data | | control | AbstractControl | The FormControl for this cell (if using FormArray with FormGroups) | | onCommit | Function | Callback to commit the value | | onCancel | Function | Callback to cancel editing |

Note: The control property is only available when:

  • The grid is bound to a FormArray (not a FormControl<T[]>)
  • The FormArray contains FormGroup controls (not raw FormControls)
  • The FormGroup has a control for the column's field name

Row-Level Validation

When using FormArray with FormGroups, you can also access row-level validation state through the FormArrayContext. This is useful for styling entire rows based on their validation state or displaying row-level error summaries.

import { getFormArrayContext, type FormArrayContext } from '@toolbox-web/grid-angular';

// Get the context from a grid element
const context = getFormArrayContext(gridElement);

if (context?.hasFormGroups) {
  // Check if row 0 is valid
  const isValid = context.isRowValid(0); // true if all controls in row are valid

  // Check if row has been touched
  const isTouched = context.isRowTouched(0); // true if any control touched

  // Check if row is dirty
  const isDirty = context.isRowDirty(0); // true if any control changed

  // Get all errors for a row
  const errors = context.getRowErrors(0);
  // Returns: { name: { required: true }, age: { min: { min: 18, actual: 15 } } }
  // Or null if no errors

  // Get the FormGroup for a row (for advanced use cases)
  const formGroup = context.getRowFormGroup(0);
}

FormArrayContext Row Validation Methods:

| Method | Return Type | Description | | ---------------------- | --------------------------------- | -------------------------------------------- | | isRowValid(idx) | boolean | True if all controls in row are valid | | isRowTouched(idx) | boolean | True if any control in row is touched | | isRowDirty(idx) | boolean | True if any control in row is dirty | | getRowErrors(idx) | Record<string, unknown> \| null | Aggregated errors from all controls, or null | | getRowFormGroup(idx) | FormGroup \| undefined | The FormGroup for the row |

Base Classes for Custom Editors & Filters

The adapter provides base classes that eliminate boilerplate when building custom editors and filter panels.

| Base Class | Extends | Purpose | | ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- | | BaseGridEditor | — | Common inputs (value, row, column, control), outputs (commit, cancel), validation helpers | | BaseGridEditorCVA | BaseGridEditor | Adds ControlValueAccessor for dual grid + standalone form use | | BaseOverlayEditor | BaseGridEditor | Floating overlay panel with CSS Anchor Positioning, focus gating, click-outside detection | | BaseFilterPanel | — | Ready-made params input for FilteringPlugin, with applyAndClose() / clearAndClose() helpers |

BaseOverlayEditor Example

import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { BaseOverlayEditor } from '@toolbox-web/grid-angular';

@Component({
  selector: 'app-date-editor',
  template: `
    <input
      #inlineInput
      readonly
      [value]="currentValue()"
      (click)="onInlineClick()"
      (keydown)="onInlineKeydown($event)"
    />
    <div #panel class="tbw-overlay-panel" style="width: 280px; padding: 12px;">
      <input type="date" [value]="currentValue()" (change)="selectAndClose($event.target.value)" />
      <button (click)="hideOverlay()">Cancel</button>
    </div>
  `,
})
export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> implements AfterViewInit {
  @ViewChild('panel') panelRef!: ElementRef<HTMLElement>;
  @ViewChild('inlineInput') inputRef!: ElementRef<HTMLInputElement>;

  protected override overlayPosition = 'below' as const;

  ngAfterViewInit(): void {
    this.initOverlay(this.panelRef.nativeElement);
    if (this.isCellFocused()) this.showOverlay();
  }

  protected getInlineInput() {
    return this.inputRef?.nativeElement ?? null;
  }
  protected onOverlayOutsideClick() {
    this.hideOverlay();
  }

  selectAndClose(date: string): void {
    this.commitValue(date);
    this.hideOverlay();
  }
}

BaseFilterPanel Example

import { Component, ViewChild, ElementRef } from '@angular/core';
import { BaseFilterPanel } from '@toolbox-web/grid-angular';

@Component({
  selector: 'app-text-filter',
  template: `
    <input #input (keydown.enter)="applyAndClose()" />
    <button (click)="applyAndClose()">Apply</button>
    <button (click)="clearAndClose()">Clear</button>
  `,
})
export class TextFilterComponent extends BaseFilterPanel {
  @ViewChild('input') input!: ElementRef<HTMLInputElement>;

  applyFilter(): void {
    this.params().applyTextFilter('contains', this.input.nativeElement.value);
  }
}

See the full Base Classes documentation for detailed API tables, all overlay positions, and CVA usage.

API Reference

Exported Directives

| Directive | Selector | Description | | ------------------ | ---------------------------------------------------- | -------------------------------------- | | Grid | tbw-grid | Main directive, auto-registers adapter | | GridFormArray | tbw-grid[formControlName], tbw-grid[formControl] | Reactive Forms integration | | TbwRenderer | *tbwRenderer | Structural directive for cell views | | TbwEditor | *tbwEditor | Structural directive for cell editors | | GridColumnView | tbw-grid-column-view | Nested directive for cell views | | GridColumnEditor | tbw-grid-column-editor | Nested directive for cell editors | | GridDetailView | tbw-grid-detail | Master-detail panel template | | GridToolPanel | tbw-grid-tool-panel | Custom sidebar panel |

Base Classes

| Class | Description | | --------------------------------- | -------------------------------------------------------------------- | | BaseGridEditor<TRow, TValue> | Base class for inline cell editors with validation helpers | | BaseGridEditorCVA<TRow, TValue> | BaseGridEditor + ControlValueAccessor for dual grid/form editors | | BaseOverlayEditor<TRow, TValue> | BaseGridEditor + floating overlay panel infrastructure | | BaseFilterPanel | Base class for custom filter panels with params input |

Type Registry

| Export | Description | | --------------------------- | -------------------------------------------- | | provideGridTypeDefaults() | Provider factory for app-level type defaults | | GridTypeRegistry | Injectable service for dynamic registration | | GRID_TYPE_DEFAULTS | Injection token for type defaults |

Icon Registry

| Export | Description | | -------------------- | ------------------------------------------------ | | provideGridIcons() | Provider factory for app-level icon overrides | | GridIconRegistry | Injectable service for dynamic icon registration | | GRID_ICONS | Injection token for icon overrides |

Grid Directive Inputs

| Input | Type | Description | | --------------- | ------------------ | ------------------------------------------------- | | gridConfig | GridConfig<TRow> | Grid config with optional component class support | | angularConfig | GridConfig<TRow> | Deprecated - use gridConfig instead | | customStyles | string | Custom CSS styles to inject into the grid |

Grid Directive Outputs

| Output | Type | Description | | ------------------- | ------------------------------------------ | ---------------------------- | | cellCommit | OutputEmitterRef<CellCommitEvent> | Cell value committed | | rowCommit | OutputEmitterRef<RowCommitEvent> | Row edit committed | | sortChange | OutputEmitterRef<SortChangeDetail> | Sort state changed | | columnResize | OutputEmitterRef<ColumnResizeDetail> | Column resized | | cellClick | OutputEmitterRef<CellClickDetail> | Cell clicked | | rowClick | OutputEmitterRef<RowClickDetail> | Row clicked | | cellActivate | OutputEmitterRef<CellActivateDetail> | Cell focus changed | | cellChange | OutputEmitterRef<CellChangeDetail> | Cell value changed | | changedRowsReset | OutputEmitterRef<ChangedRowsResetDetail> | Changed rows cache cleared | | filterChange | OutputEmitterRef<FilterChangeDetail> | Filter state changed | | columnMove | OutputEmitterRef<ColumnMoveDetail> | Column moved | | columnVisibility | OutputEmitterRef<ColumnVisibilityDetail> | Column visibility changed | | columnStateChange | OutputEmitterRef<GridColumnState> | Column state changed | | selectionChange | OutputEmitterRef<SelectionChangeDetail> | Selection changed | | rowMove | OutputEmitterRef<RowMoveDetail> | Row moved (drag & drop) | | groupToggle | OutputEmitterRef<GroupToggleDetail> | Group expanded/collapsed | | treeExpand | OutputEmitterRef<TreeExpandDetail> | Tree node expanded/collapsed | | detailExpand | OutputEmitterRef<DetailExpandDetail> | Detail panel toggled | | responsiveChange | OutputEmitterRef<ResponsiveChangeDetail> | Responsive mode changed | | copy | OutputEmitterRef<CopyDetail> | Data copied to clipboard | | paste | OutputEmitterRef<PasteDetail> | Data pasted from clipboard |

GridDetailView Inputs

| Input | Type | Default | Description | | ------------------ | ---------------------------- | --------- | ----------------------------------- | | showExpandColumn | boolean | true | Show expand/collapse chevron column | | animation | 'slide' \| 'fade' \| false | 'slide' | Animation style for expand/collapse |

GridToolPanel Inputs

| Input | Type | Default | Description | | --------- | -------- | -------- | --------------------------------- | | id | string | Required | Unique panel identifier | | title | string | Required | Panel title in accordion header | | icon | string | - | Icon for the accordion header | | tooltip | string | - | Tooltip text for header | | order | number | 100 | Panel sort order (lower = higher) |

Exported Types

import type {
  // Template contexts
  GridCellContext,
  GridEditorContext,
  GridDetailContext,
  GridToolPanelContext,
  StructuralCellContext,
  StructuralEditorContext,
  // Events
  CellCommitEvent,
  RowCommitEvent,
  // Primary config exports - use these
  GridConfig,
  ColumnConfig,
  CellRenderer,
  CellEditor,
  TypeDefault,
  // Deprecated aliases
  AngularGridConfig,
  AngularColumnConfig,
  AngularCellRenderer,
  AngularCellEditor,
  AngularTypeDefault,
  // Reactive Forms
  FormArrayContext,
  // Overlay position type
  OverlayPosition,
} from '@toolbox-web/grid-angular';

// Base classes for custom editors and filter panels
import { BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, BaseFilterPanel } from '@toolbox-web/grid-angular';

// Type guard for component class detection
import { isComponentClass } from '@toolbox-web/grid-angular';

// Helper to access form context from grid element
import { getFormArrayContext } from '@toolbox-web/grid-angular';

AngularGridAdapter

The adapter class is exported for advanced use cases:

import { AngularGridAdapter } from '@toolbox-web/grid-angular';

In most cases, the Grid directive handles adapter registration automatically.

Demo

See the full Angular demo at demos/employee-management/angular/ which demonstrates:

  • 15+ plugins with full configuration
  • Custom editors (star rating, date picker, status select, bonus slider)
  • Custom renderers (status badges, rating colors, top performer stars)
  • Structural directives with auto-wiring
  • Signal-based reactivity
  • Shell integration (header, tool panels)
  • Master-detail expandable rows

Requirements

  • Angular 17+ (standalone components)
  • @toolbox-web/grid >= 0.2.0

Development

# Build the library
bun nx build grid-angular

# Run tests
bun nx test grid-angular

# Lint
bun nx lint grid-angular

Support This Project

This grid is built and maintained by a single developer in spare time. If it saves you time or money, consider sponsoring to keep development going:

GitHub Sponsors Patreon


License

MIT