Skip to main content
Partas

Adding Selectable Rows

As an example, let's make the rows selectable, and provide a 'select-all' button too.

The first thing we'll want is to make the button to select the rows. Let's make it a checkbox.

For the sake of this example (and because I can just rip off what I've already written before), we're going to use an accessible headless ui library @kobalte/core. We'll also use lucide icons. This will show how the headless UI library is used (as it is the main one that is bound in Partas.Solid).

Checkbox

Here's the checkbox unstyled, with some width/height and colour added so its visible:

tip

If you inspect the element and click it, you'll see the checked data attribute appear.

For the Checkbox component, we're going to inherit and implement the Kobalte.Checkbox component:

open Partas.Solid.Kobalte
[<Erase>]
type Checkbox() =
inherit Kobalte.Checkbox()

Let's add our implementation.

[<SolidTypeComponent>]
member props.__ =
Kobalte.Checkbox(
class' = Lib.cn [|
"items-top group relative flex space-x-2"
props.class'
|]
).spread(props)

There's a few accessibility related components to put in the Checkbox, such as an Input, Control and Indicator. But if we try to add them, we might run into a type problem. The current implementation seems to have an issue with the ChildLambdaProviders where we have no choice but to use the lambda child provider.

tip

If we look at the tooltip for Kobalte.Checkbox we see:

type Checkbox =
interface ChildLambdaProvider<CheckboxRenderProp>
interface Polymorph
interface HtmlTag

This means the lambda takes one parameter which is CheckboxRenderProp.

open Partas.Solid.Lucide
[<SolidTypeComponent>]
member props.checkbox =
Kobalte.Checkbox(
indeterminate = props.indeterminate,
class' = Lib.cn [| "items-top group relative flex space-x-2"; props.class' |]
).spread(props) { yield fun _ -> Fragment() {
Checkbox.Input(class'="peer")
Checkbox.Control(
class' = "size-4 shrink-0 rounded-sm border border-primary
ring-offset-background disabled:cursor-not-allowed disabled:opacity-50
peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring
peer-focus-visible:ring-offset-2 data-[checked]:border-none
data-[indeterminate]:border-none data-[checked]:bg-primary
data-[indeterminate]:bg-primary data-[checked]:text-primary-foreground
data-[indeterminate]:text-primary-foreground"
) {
Checkbox.Indicator() {
if props.indeterminate then
Minus(class' = "size-4", strokeWidth = 2)
else
Check(class' = "size-4", strokeWidth = 2)
}
}
}
}
note

Because we want the indeterminate property to be passed to the root component, but also use it to conditionally render something, we have to ensure we also pass it by name to the root component, since it is automatically split from the props object.

Using the CheckboxRenderProps instead

The ChildLambdaProvider for the Checkbox passes it's state to its children.

We can instead access the indeterminate status from this, and thereby not worry about the property being split off our props.

[<SolidTypeComponent>]
member props.checkbox =
Kobalte.Checkbox(
indeterminate = props.indeterminate,
class' = Lib.cn [| "items-top group relative flex space-x-2"; props.class' |]
).spread(props) { yield fun rprops -> Fragment() {
Checkbox.Input(class'="peer")
Checkbox.Control(
class' = "size-4 shrink-0 rounded-sm border border-primary
ring-offset-background disabled:cursor-not-allowed disabled:opacity-50
peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring
peer-focus-visible:ring-offset-2 data-[checked]:border-none
data-[indeterminate]:border-none data-[checked]:bg-primary
data-[indeterminate]:bg-primary data-[checked]:text-primary-foreground
data-[indeterminate]:text-primary-foreground"
) {
Checkbox.Indicator() {
if rprops.indeterminate then
Minus(class' = "size-4", strokeWidth = 2)
else
Check(class' = "size-4", strokeWidth = 2)
}
}
}
}

Let's give that a spin:

Checkbox

Column Definition

We add a column definition for the 'selection column' where we'll put our checkbox to indicate it has been selected.

In this case, we use the proper signatures for the header and cell properties of ColumnDef, which feed the relevant parameters of the row into the definition.

open Partas.Solid.Aria
[<SolidComponent>]
let selectColumn =
ColumnDef<User>(
id = "select"
,enableHiding = false
,cell = fun props ->
Checkbox(
checked' = props.row.getIsSelected(),
onChange = fun value -> props.row.toggleSelected(!!value)
,ariaLabel = "Select row",
class' = "translate-y-[2px]"
)
,header = fun props ->
Checkbox(
checked' = (props.table.getIsAllPageRowsSelected()),
indeterminate = (props.table.getIsSomePageRowsSelected()),
onChange = fun value -> props.table.toggleAllPageRowsSelected(!!value)
,ariaLabel = "Select all"
,class' = "translate-y-[2px]"
)
)
let columnDefs = [|
selectColumn
codeColumn
nameColumn
colorColumn
|]
note

To identify the column when the header is not a simple string, we pass a unique id

Adding Selection to Table

Adjust the table options in our DataTable component to include a selection state.

First we create a reactive signal of the selection state:

[<SolidComponent>]
let TestSelectableTable () =
let selection,setSelection =
createSignal <| RowSelectionState.init()
let table = createTable(
TableOptions<User>(
getCoreRowModel = getCoreRowModel()
) .data(fun _ -> userData)
.columns(fun _ -> columnDefs)
)
DataTable(table = table)

And now we plug that into the createTable TableOptions.

[<SolidComponent>]
let TestSelectableTable () =
let selection,setSelection =
createSignal <| RowSelectionState.init()
let table = createTable(
TableOptions<User>(
getCoreRowModel = getCoreRowModel(),
enableRowSelection = !!true,
onRowSelectionChange = !!setSelection
) .data(fun _ -> userData)
.columns(fun _ -> columnDefs)
.stateFn(fun state ->
state.rowSelection(selection)
)
)
DataTable(table = table)
What is stateFn?

Just like data and columns, to make the row selections reactive, we need them to be getter object properties.

The .stateFn passes and object which makes this easy for us to do in Fable.

Now lets render our table and see what we get:

CodeNameColor
Code
Name
Color
Code1
Name1
Color1
Code2
Name2
Color2
Example Code
module Partas.Solid.examples.checkbox
open Partas.Solid.Lucide
open Partas.Solid
open Partas.Solid.Aria
open Partas.Solid.TanStack.Table
open Partas.Solid.Kobalte
open Fable.Core
open Fable.Core.JsInterop
type [<Erase>] Lib =
[<Import("twMerge", "tailwind-merge")>]
static member twMerge (classes: string) : string = jsNative
[<Import("clsx", "clsx")>]
static member clsx(classes: obj): string = jsNative
static member cn (classes: string array): string = classes |> Lib.clsx |> Lib.twMerge
[<Erase>]
type Checkbox() =
inherit Kobalte.Checkbox()
[<SolidTypeComponent>]
member props.checkbox =
Kobalte.Checkbox(
indeterminate = props.indeterminate,
class' = Lib.cn [| "items-top group relative flex space-x-2"; props.class' |]
).spread(props) { yield fun _ -> Fragment() {
Checkbox.Input(class'="peer")
Checkbox.Control(
class' = "size-4 shrink-0 rounded-sm border border-primary
ring-offset-background disabled:cursor-not-allowed disabled:opacity-50
peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring
peer-focus-visible:ring-offset-2 data-[checked]:border-none
data-[indeterminate]:border-none data-[checked]:bg-primary
data-[indeterminate]:bg-primary data-[checked]:text-primary-foreground
data-[indeterminate]:text-primary-foreground"
) {
Checkbox.Indicator() {
if props.indeterminate then
Minus(class' = "size-4", strokeWidth = 2)
else
Check(class' = "size-4", strokeWidth = 2)
}
}
}
}
[<Erase>]
type TableCaption() =
inherit caption()
[<SolidTypeComponent>]
member props.__ =
caption(class' = Lib.cn [|
"mt-4 text-sm text-muted-foreground"
props.class'
|]).spread props
[<Erase>]
type Table() =
inherit table()
[<SolidTypeComponent>]
member props.__ =
div(class' = "relative w-full overflow-auto") {
table(class' = Lib.cn [|
"w-full caption-bottom text-sm"
props.class'
|]).spread props
}
[<Erase>]
type TableHeader() =
inherit thead()
[<SolidTypeComponent>]
member props.constructor =
thead(class' = Lib.cn [| "[&_tr]:border-b"
props.class' |])
.spread props
[<Erase>]
type TableBody() =
inherit tbody()
[<SolidTypeComponent>]
member props.constructor =
tbody(class' = Lib.cn [|
"[&_tr:last-child]:border-0"
props.class'
|]).spread props
[<Erase>]
type TableFooter() =
inherit tfoot()
[<SolidTypeComponent>]
member props.constructor =
tfoot(class' = Lib.cn [|
"bg-primary font-medium text-primary-foreground"
props.class'
|]).spread props
[<Erase>]
type TableRow() =
inherit tr()
[<SolidTypeComponent>]
member props.constructor =
tr(
class' = Lib.cn [|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
props.class'
|]
).spread props
[<Erase>]
type TableHead() =
inherit th()
[<SolidTypeComponent>]
member props.constructor =
th(
class' = Lib.cn [|
"h-10 px-2 text-left align-middle font-medium
text-muted-foreground [&:has([role=checkbox])]:pr-0"
props.class'
|]
).spread props
[<Erase>]
type TableCell() =
inherit td()
[<SolidTypeComponentAttribute>]
member props.constructor =
td(class' = Lib.cn [| "p-2 align-middle [&:has([role=checkbox])]:pr-0"; props.class' |]).spread props
[<Erase>]
type DataTable<'T>() =
interface VoidNode
[<DefaultValue>] val mutable table: Table<'T>
[<SolidTypeComponent>]
member props.__ =
let table = props.table
Table() {
TableHeader() {
For(each = table.getHeaderGroups()) {
yield fun headerGroup _ ->
TableRow() {
For(each = headerGroup.headers) {
yield fun header _ ->
TableHead(colspan = header.colSpan) {
Show(when' = not header.isPlaceholder) {
flexRender(header.column.columnDef.header, header.getContext())
}
}
}
}
}
}
TableBody() {
Show(
when' = unbox (table.getRowModel().rows.Length)
,fallback = (TableRow() { TableCell(colspan = (8), class' = "h-24 text-center") { "No Results." } })
) {
For(each = table.getRowModel().rows) {
yield fun row _ ->
TableRow().data("state", !!(row.getIsSelected() && !!"selected")) {
For(each = row.getVisibleCells()) {
yield fun cell _ ->
TableCell() {
flexRender(cell.column.columnDef.cell, cell.getContext())
}
}
}
}
}
}
}
type User = {
Code: string
Name: string
Color: string
}
[<SolidComponent>]
let codeColumn = ColumnDef<User>(
accessorFn = fun user _ -> user.Code
,id = "code"
,header = !!"Code"
,cell = fun props ->
div(class' = "w-14 hover:scale-102 flex justify-center bg-black text-white") {
props.getValue() :?> string
}
)
[<SolidComponent>]
let nameColumn = ColumnDef<User>(
accessorFn = fun user _ -> user.Name
,header = !!"Name"
,cell = fun props ->
div(class' = "w-14 hover:scale-102 flex justify-center bg-black text-white") {
props.getValue() :?> string
}
)
[<SolidComponent>]
let colorColumn = ColumnDef<User>(
accessorFn = fun user _ -> user.Color
,header = !!"Color"
,cell = fun props ->
div(class' = "w-14 hover:scale-102 flex justify-center bg-black text-white") {
props.getValue() :?> string
}
)
[<SolidComponent>]
let selectColumn =
ColumnDef<User>(
id = "select"
,enableHiding = false
,cell = fun cellProps ->
Checkbox(
checked' = cellProps.row.getIsSelected(),
onChange = fun value -> cellProps.row.toggleSelected(!!value)
,ariaLabel = "Select row",
class' = "translate-y-[2px]"
)
,header = fun headerProps ->
Checkbox(
checked' = (headerProps.table.getIsAllPageRowsSelected()),
indeterminate = (headerProps.table.getIsSomePageRowsSelected()),
onChange = fun value -> headerProps.table.toggleAllPageRowsSelected(!!value)
,ariaLabel = "Select all"
,class' = "translate-y-[2px]"
)
)
let columnDefs = [|
selectColumn
codeColumn
nameColumn
colorColumn
|]
let userData = [|
{ Name = "Name"; Color = "Color"; Code = "Code" }
{ Name = "Name1"; Color = "Color1"; Code = "Code1" }
{ Name = "Name2"; Color = "Color2"; Code = "Code2" }
|]
[<SolidComponent>]
let TestSelectableTable () =
let selection,setSelection =
createSignal <| RowSelectionState.init()
let table = createTable(
TableOptions<User>(
getCoreRowModel = getCoreRowModel(),
enableRowSelection = !!true,
onRowSelectionChange = !!setSelection
) .data(fun _ -> userData)
.columns(fun _ -> columnDefs)
.stateFn(fun state ->
state.rowSelection(selection)
)
)
DataTable(table = table)
import { twMerge } from "tailwind-merge";
import { clsx } from "clsx";
import { createSignal, Show, For, splitProps } from "solid-js";
import { Indicator, Control, Input, Root } from "@kobalte/core/checkbox";
import Minus from "lucide-solid/icons/minus";
import Check from "lucide-solid/icons/check";
import { getCoreRowModel, createSolidTable, flexRender } from "@tanstack/solid-table";
import { Record } from "../fable_modules/fable-library-js.5.0.0-alpha.13/Types.js";
import { record_type, string_type } from "../fable_modules/fable-library-js.5.0.0-alpha.13/Reflection.js";
export function Lib_cn_Z35CD86D0(classes) {
return twMerge(clsx(classes));
}
export function Checkbox(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["indeterminate", "class"]);
return <Root indeterminate={PARTAS_LOCAL.indeterminate}
class={Lib_cn_Z35CD86D0(["items-top group relative flex space-x-2", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false}>
{(_arg) => <>
<Input class="peer" />
<Control class="size-4 shrink-0 rounded-sm border border-primary
ring-offset-background disabled:cursor-not-allowed disabled:opacity-50
peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring
peer-focus-visible:ring-offset-2 data-[checked]:border-none
data-[indeterminate]:border-none data-[checked]:bg-primary
data-[indeterminate]:bg-primary data-[checked]:text-primary-foreground
data-[indeterminate]:text-primary-foreground">
<Indicator>
{PARTAS_LOCAL.indeterminate ? (<Minus class="size-4"
strokeWidth={2} />) : (<Check class="size-4"
strokeWidth={2} />)}
</Indicator>
</Control>
</>}
</Root>;
}
export function TableCaption(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <caption class={Lib_cn_Z35CD86D0(["mt-4 text-sm text-muted-foreground", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function Table(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <div class="relative w-full overflow-auto">
<table class={Lib_cn_Z35CD86D0(["w-full caption-bottom text-sm", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />
</div>;
}
export function TableHeader(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <thead class={Lib_cn_Z35CD86D0(["[&_tr]:border-b", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function TableBody(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <tbody class={Lib_cn_Z35CD86D0(["[&_tr:last-child]:border-0", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function TableFooter(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <tfoot class={Lib_cn_Z35CD86D0(["bg-primary font-medium text-primary-foreground", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function TableRow(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <tr class={Lib_cn_Z35CD86D0(["border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function TableHead(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <th class={Lib_cn_Z35CD86D0(["h-10 px-2 text-left align-middle font-medium\r\n text-muted-foreground [&:has([role=checkbox])]:pr-0", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function TableCell(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["class"]);
return <td class={Lib_cn_Z35CD86D0(["p-2 align-middle [&:has([role=checkbox])]:pr-0", PARTAS_LOCAL.class])}
{...PARTAS_OTHERS} bool:n$={false} />;
}
export function DataTable(props) {
const [PARTAS_LOCAL, PARTAS_OTHERS] = splitProps(props, ["table"]);
const table = PARTAS_LOCAL.table;
return <Table>
<TableHeader>
<For each={table.getHeaderGroups()}>
{(headerGroup, _arg) => <TableRow>
<For each={headerGroup.headers}>
{(header, _arg_1) => <TableHead colspan={header.colSpan}>
<Show when={!header.isPlaceholder}>
{flexRender(header.column.columnDef.header, header.getContext())}
</Show>
</TableHead>}
</For>
</TableRow>}
</For>
</TableHeader>
<TableBody>
<Show when={table.getRowModel().rows.length}
fallback={<TableRow>
<TableCell colspan={8}
class="h-24 text-center">
No Results.
</TableCell>
</TableRow>}>
<For each={table.getRowModel().rows}>
{(row, _arg_2) => <TableRow data-state={row.getIsSelected() && "selected"}>
<For each={row.getVisibleCells()}>
{(cell, _arg_3) => <TableCell>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>}
</For>
</TableRow>}
</For>
</Show>
</TableBody>
</Table>;
}
export class User extends Record {
constructor(Code, Name, Color) {
super();
this.Code = Code;
this.Name = Name;
this.Color = Color;
}
}
export function User_$reflection() {
return record_type("Partas.Solid.examples.checkbox.User", [], User, () => [["Code", string_type], ["Name", string_type], ["Color", string_type]]);
}
export const codeColumn = {
id: "code",
accessorFn: (user, _arg) => user.Code,
header: "Code",
cell: (props) => <div class="w-14 hover:scale-102 flex justify-center bg-black text-white">
{props.getValue()}
</div>,
};
export const nameColumn = {
accessorFn: (user, _arg) => user.Name,
header: "Name",
cell: (props) => <div class="w-14 hover:scale-102 flex justify-center bg-black text-white">
{props.getValue()}
</div>,
};
export const colorColumn = {
accessorFn: (user, _arg) => user.Color,
header: "Color",
cell: (props) => <div class="w-14 hover:scale-102 flex justify-center bg-black text-white">
{props.getValue()}
</div>,
};
export const selectColumn = {
id: "select",
header: (headerProps) => <Checkbox checked={headerProps.table.getIsAllPageRowsSelected()}
indeterminate={headerProps.table.getIsSomePageRowsSelected()}
onChange={(value) => {
headerProps.table.toggleAllPageRowsSelected(value);
}}
ariaLabel="Select all"
class="translate-y-[2px]" />,
cell: (cellProps) => <Checkbox checked={cellProps.row.getIsSelected()}
onChange={(value_1) => {
cellProps.row.toggleSelected(value_1);
}}
ariaLabel="Select row"
class="translate-y-[2px]" />,
enableHiding: false,
};
export const columnDefs = [selectColumn, codeColumn, nameColumn, colorColumn];
export const userData = [new User("Code", "Name", "Color"), new User("Code1", "Name1", "Color1"), new User("Code2", "Name2", "Color2")];
export function TestSelectableTable() {
let this$_6, this$_3, this$_1, this$_5;
const patternInput = createSignal({});
const table = createSolidTable((this$_6 = ((this$_3 = ((this$_1 = {
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
onRowSelectionChange: patternInput[1],
}, (Object.defineProperty(this$_1, "data", {
get: () => userData,
}), this$_1))), (Object.defineProperty(this$_3, "columns", {
get: () => columnDefs,
}), this$_3))), (this$_6.state = ((this$_5 = {}, (void Object.defineProperty(this$_5, "rowSelection", {
get: patternInput[0],
}), this$_5))), this$_6)));
return <DataTable table={table} />;
}

Conclusion

That's it for this little exercise; the idea was to dip toes into using some of the binding libraries available and familiarising ourselves with Partas.Solid.

When it comes to DataTables, undoubtedly @tanstack/table is one of the most well known. It is worth exploring and learning the API for this mammoth binding.

Last updated: 7/10/25, 1:24 PM

PartasBuilt using the Partas.SolidStart SolidBase template
Community
githubdiscord