# Example: Custom cell editor

A renderEditor select bridges a numeric priority column to and from the string a native control hands back, via formatEditValue and parseEditValue.

Source: https://pretable.ai/examples/custom-cell-editor.md

```tsx CustomEditorGrid.tsx
"use client";

import { useState } from "react";

import { PretableSurface } from "@pretable/react";

import { columns } from "./columns";
import { tasks, type Task } from "./data";

const VIEWPORT_HEIGHT = 200;

export function CustomEditorGrid() {
  const [rows, setRows] = useState<Task[]>(tasks);

  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        <strong>Title</strong> uses the built-in text editor. Double-click (or
        press <kbd>Enter</kbd>) on a <strong>Priority</strong> cell to open the
        custom <code>renderEditor</code> below — a plain
        <code>{"<select>"}</code>, bridged to the numeric stored value by{" "}
        <code>formatEditValue</code> and <code>parseEditValue</code>.
      </p>
      <PretableSurface<Task>
        ariaLabel="Tasks"
        columns={columns}
        getRowId={(row) => row.id}
        rows={rows}
        viewportHeight={VIEWPORT_HEIGHT}
        onRowChange={({ rowId, row }) => {
          setRows((previous) =>
            previous.map((candidate) =>
              candidate.id === rowId ? row : candidate,
            ),
          );
        }}
      />
    </div>
  );
}
```

```tsx columns.tsx
import type { PretableColumn } from "@pretable/react";

import type { Task } from "./data";

const PRIORITY_LABEL: Record<number, string> = {
  1: "Low",
  2: "Medium",
  3: "High",
};

export const columns: PretableColumn<Task>[] = [
  { id: "title", header: "Title", editable: true, widthPx: 220 },
  {
    id: "priority",
    header: "Priority",
    editable: true,
    widthPx: 130,
    render: ({ row }) => PRIORITY_LABEL[row.priority] ?? String(row.priority),
    // The stored value is a number; a native <select> only ever hands back
    // strings on change, so formatEditValue seeds the draft as a string and
    // parseEditValue converts it back on commit.
    formatEditValue: (value) => String(value),
    parseEditValue: (raw) => Number(raw),
    renderEditor: ({ draft, setDraft, commit, cancel }) => (
      <select
        autoFocus
        value={String(draft ?? "")}
        onChange={(e) => setDraft(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Escape") cancel();
        }}
        onBlur={() => commit()}
      >
        <option value="1">Low</option>
        <option value="2">Medium</option>
        <option value="3">High</option>
      </select>
    ),
  },
];
```

```ts data.ts
export interface Task {
  id: string;
  title: string;
  // 1 = Low, 2 = Medium, 3 = High.
  priority: number;
}

export const tasks: Task[] = [
  { id: "t1", title: "Draft proposal", priority: 2 },
  { id: "t2", title: "Review PR #482", priority: 3 },
  { id: "t3", title: "Update changelog", priority: 1 },
  { id: "t4", title: "Fix flaky test", priority: 3 },
];
```
