# Multiple Selection

```ts
import { Grid, Checkbox } from 'cx/widgets';
```



Grid supports multiple row selection using checkboxes. A "select all" checkbox in the header can toggle selection for all visible rows, with support for the indeterminate state when only some rows are selected.

```tsx
import { createModel, updateArray } from "cx/data";
import { Controller, PropertySelection } from "cx/ui";
import { getSearchQueryPredicate } from "cx/util";
import { Checkbox, Grid, TextField } from "cx/widgets";

import "../../icons/lucide";

interface Employee {
  id: number;
  fullName: string;
  phone: string;
  city: string;
  selected: boolean;
}

interface PageModel {
  records: Employee[];
  searchText: string;
  selectAll: boolean | null;
  $record: Employee;
}

const m = createModel<PageModel>();

class PageController extends Controller {
  visibleIdsMap: Record<number, boolean> = {};

  onInit() {
    this.store.set(
      m.records,
      Array.from({ length: 10 }, (_, i) => ({
        id: i + 1,
        fullName:
          [
            "Alice Johnson",
            "Bob Smith",
            "Carol White",
            "David Brown",
            "Eva Green",
          ][i % 5] +
          " " +
          (i + 1),
        phone: `555-${String(1000 + i).padStart(4, "0")}`,
        city: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"][
          i % 5
        ],
        selected: false,
      })),
    );

    // Handle "select all" checkbox clicks
    this.addTrigger("select-all-click", [m.selectAll], (value) => {
      if (value != null) {
        this.store.update(
          m.records,
          updateArray,
          (r: Employee) => ({ ...r, selected: value }),
          (r: Employee) => this.visibleIdsMap[r.id],
        );
      }
    });
  }

  // Track visible records and update "select all" state
  updateSelection(newRecords: { data: Employee }[]) {
    this.visibleIdsMap = newRecords.reduce(
      (acc, r) => {
        acc[r.data.id] = true;
        return acc;
      },
      {} as Record<number, boolean>,
    );

    let anySelected = false;
    let anyUnselected = false;

    for (const rec of newRecords) {
      if (rec.data.selected) anySelected = true;
      else anyUnselected = true;
      if (anySelected && anyUnselected) break;
    }

    // null = indeterminate, true = all selected, false = none selected
    this.store.set(
      m.selectAll,
      anySelected && anyUnselected ? null : !!anySelected,
    );
  }
}

export default (
  <div controller={PageController}>
    <TextField
      value={m.searchText}
      icon="search"
      placeholder="Search..."
      showClear
      style="width: 300px; margin-bottom: 16px"
    />
    <Grid
      records={m.records}
      style="width: 100%"
      filterParams={m.searchText}
      onCreateFilter={(searchText: string) => {
        if (!searchText) return () => true;
        const predicate = getSearchQueryPredicate(searchText);
        return (record: Employee) =>
          predicate(record.fullName) ||
          predicate(record.phone) ||
          predicate(record.city);
      }}
      onTrackMappedRecords={(records, instance) =>
        instance.getControllerByType(PageController).updateSelection(records)
      }
      selection={{ type: PropertySelection, multiple: true }}
      columns={[
        {
          header: {
            style: "padding: 4px 6px",
            children: (
              <Checkbox
                value={m.selectAll}
                indeterminate
                unfocusable
                class="p-0"
              />
            ),
          },
          field: "selected",
          style: "width: 1px",
          pad: false,
          align: "center",
          children: (
            <Checkbox value={m.$record.selected} unfocusable class="p-0" />
          ),
        },
        { header: "Name", field: "fullName", sortable: true },
        { header: "Phone", field: "phone" },
        { header: "City", field: "city", sortable: true },
      ]}
    />
  </div>
);

```

Click checkboxes to select individual rows. Use the header checkbox to select or deselect all visible rows. Try filtering to see how "select all" works with filtered data.

## How It Works

The example combines several features:

1. **Checkbox column** - A column with checkboxes bound to each record's `selected` property
2. **Select all checkbox** - A header checkbox that toggles all visible rows
3. **Indeterminate state** - The header checkbox shows indeterminate when some (but not all) rows are selected
4. **Filtered selection** - "Select all" only affects currently visible (filtered) rows

The `onTrackMappedRecords` callback is called whenever the visible records change (due to filtering or sorting). It receives the current visible records, allowing the controller to track which records are visible and update the "select all" checkbox state accordingly.

```tsx
onTrackMappedRecords={(records, instance) =>
  instance.getControllerByType(PageController).updateSelection(records)
}
```

## Configuration

| Property | Type | Description |
| -------- | ---- | ----------- |
| `selection` | `object` | Selection configuration. Use `PropertySelection` with `multiple: true` for checkbox-based selection. |
| `onTrackMappedRecords` | `string \| function` | Callback invoked when visible records change. Receives an array of record instances. |

See also: [Searching and Filtering](/docs/tables/searching-and-filtering), [Selections](/docs/concepts/selections)