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:
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.
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) } } } }
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:
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|]
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:
Code | Name | Color | |
---|---|---|---|
Code | Name | Color | |
Code1 | Name1 | Color1 | |
Code2 | Name2 | Color2 |
Example Code
module Partas.Solid.examples.checkbox
open Partas.Solid.Lucideopen Partas.Solidopen Partas.Solid.Ariaopen Partas.Solid.TanStack.Tableopen Partas.Solid.Kobalteopen Fable.Coreopen 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