# Searching Tree Grids

```ts
import { findTreeNode } from 'cx/data';
```

Tree grids require special handling for search because a parent node should remain visible if any of its descendants match the search query.

```tsx
import { createModel, findTreeNode } from "cx/data";
import { Controller, TreeAdapter } from "cx/ui";
import { Grid, TextField, TreeNode } from "cx/widgets";

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

interface TreeRecord {
  id: number;
  name: string;
  city: string;
  $leaf?: boolean;
  $expanded?: boolean;
  $level?: number;
  $children?: TreeRecord[];
}

interface PageModel {
  search: string;
  data: TreeRecord[];
  $record: TreeRecord;
}

const m = createModel<PageModel>();

function isMatch(node: TreeRecord, search: string) {
  return (
    node.name.toLowerCase().includes(search) ||
    node.city.toLowerCase().includes(search)
  );
}

class PageController extends Controller {
  onInit() {
    this.store.set(m.data, [
      {
        id: 1,
        name: "North America",
        city: "",
        $leaf: false,
        $expanded: true,
        $children: [
          { id: 2, name: "Alice Johnson", city: "New York", $leaf: true },
          { id: 3, name: "Bob Smith", city: "Los Angeles", $leaf: true },
          {
            id: 4,
            name: "West Coast",
            city: "",
            $leaf: false,
            $expanded: true,
            $children: [
              { id: 5, name: "Carol White", city: "Seattle", $leaf: true },
              { id: 6, name: "David Brown", city: "Portland", $leaf: true },
            ],
          },
        ],
      },
      {
        id: 7,
        name: "Europe",
        city: "",
        $leaf: false,
        $expanded: true,
        $children: [
          { id: 8, name: "Eva Green", city: "London", $leaf: true },
          { id: 9, name: "Frank Miller", city: "Paris", $leaf: true },
        ],
      },
    ]);
  }
}

export default (
  <div controller={PageController}>
    <TextField
      value={m.search}
      icon="search"
      placeholder="Search..."
      style="margin-bottom: 16px"
    />
    <Grid
      records={m.data}
      mod="tree"
      style="height: 350px"
      scrollable
      dataAdapter={{ type: TreeAdapter }}
      emptyText="No records match the search"
      filterParams={m.search}
      onCreateFilter={(search: string) => {
        if (!search) return () => true;
        search = search.toLowerCase();
        return (node: TreeRecord) => {
          if (isMatch(node, search)) return true;
          if (node.$leaf || !node.$children) return false;
          const result = findTreeNode(
            node.$children,
            (subNode: TreeRecord) => isMatch(subNode, search),
            "$children",
          );
          return result ? true : false;
        };
      }}
      columns={[
        {
          header: "Name",
          field: "name",
          children: (
            <TreeNode
              expanded={m.$record.$expanded}
              leaf={m.$record.$leaf}
              level={m.$record.$level}
              text={m.$record.name}
            />
          ),
        },
        { header: "City", field: "city" },
      ]}
    />
  </div>
);

```

Try searching for "Seattle" or "Europe" to see how parent nodes remain visible when children match.

## How It Works

When filtering a flat grid, you simply check if each record matches. With trees, the logic is:
1. Show a node if it matches the search
2. Show a parent node if any descendant matches (even if the parent doesn't match)
3. Hide leaf nodes that don't match

The `findTreeNode` utility recursively searches through a tree to find matching descendants. It takes three arguments:
- `nodes` - Array of nodes to search
- `predicate` - Function that returns `true` when a match is found
- `childrenField` - Name of the property containing child nodes (default: `"$children"`)

Returns the first matching node or `null` if no match is found.

## Performance Considerations

This approach calls the filter function for each node in the tree. For each non-leaf node, it may also traverse the entire subtree to find matches.

For very large trees (thousands of nodes), consider:
- Server-side filtering that returns only matching branches
- Caching search results
- Debouncing the search input

## Configuration

| Property | Type | Description |
| -------- | ---- | ----------- |
| `filterParams` | `Prop` | Search parameters passed to `onCreateFilter`. |
| `onCreateFilter` | `function` | Returns a predicate `(node) => boolean` for filtering. |
| `emptyText` | `string` | Text shown when no nodes match the filter. |