Creating a Cell Plugin
Build a custom property type for Table View.
A cell plugin defines the data model for one property type. It must render a cell value; add an editor when users should be able to change that value.
Create a Tag plugin
Define the plugin
import type {
CellEditorProps,
CellPlugin,
CellValueProps,
} from "@notion-kit/table-hook/plugins";
import { createCompareFn } from "@notion-kit/table-hook/plugins";
type TagData = string[];
type TagConfig = { options: string[] };
type TagPlugin = CellPlugin<"tag", TagData, TagConfig>;
const TagIcon = <span aria-hidden>🏷️</span>;
function TagValue({ data }: Pick<CellValueProps<TagData, TagConfig>, "data">) {
return <span>{data.join(", ") || "—"}</span>;
}
function TagEditor({
data,
config,
onChange,
}: Pick<CellEditorProps<TagData, TagConfig>, "data" | "config" | "onChange">) {
return (
<div>
{config.options.map((tag) => {
const selected = data.includes(tag);
return (
<button
key={tag}
type="button"
aria-pressed={selected}
onClick={() =>
onChange(
selected
? data.filter((value) => value !== tag)
: [...data, tag],
)
}
>
{tag}
</button>
);
})}
</div>
);
}
export const tagPlugin: TagPlugin = {
id: "tag",
meta: {
name: "Tag",
desc: "A multi-select tag property",
icon: TagIcon,
},
default: {
name: "Tags",
icon: TagIcon,
config: { options: [] },
data: [],
},
fromValue: (value) =>
typeof value === "string"
? value
.split(",")
.map((tag) => tag.trim())
.filter(Boolean)
: [],
toValue: (data) => data.join(", "),
toTextValue: (data) => data.join(", "),
compare: createCompareFn<TagPlugin>((a, b) =>
a.join(", ").localeCompare(b.join(", ")),
),
renderCellValue: (props) => <TagValue {...props} />,
renderCellEditor: (props) => ({
presentation: "popover",
content: <TagEditor {...props} />,
}),
};renderCellEditor receives a scope. It is { kind: "cell" } for a single
cell and { kind: "bulk" } when the bulk-edit bar invokes the same editor for
selected rows. The example does not need to branch on the scope, so it supports
both paths automatically.
Register the plugin
Add the plugin to the list passed to TableView, then include a matching
property in the table data.
import { DEFAULT_PLUGINS, TableView } from "@notion-kit/table-view";
import type { ColumnDefs, Row } from "@notion-kit/table-view";
import { tagPlugin } from "./tag-plugin";
const plugins = [...DEFAULT_PLUGINS, tagPlugin];
const properties: ColumnDefs<typeof plugins> = [
{ id: "name", name: "Name", type: "title" },
{
id: "tags",
name: "Tags",
type: "tag",
config: { options: ["Bug", "Feature"] },
},
];
const now = Date.now();
const data: Row<typeof plugins>[] = [
{
id: "task-1",
createdAt: now,
lastEditedAt: now,
properties: {
name: { id: "task-1-name", value: "Ship Table View" },
tags: { id: "task-1-tags", value: ["Feature"] },
},
},
];
export function DatabaseView() {
return (
<TableView
plugins={plugins}
defaultProperties={properties}
defaultData={data}
/>
);
}Add renderConfigMenu when users need to edit the property configuration, and
add sorting, grouping, or counting methods only when the property needs those
capabilities.