Skip to content

Selectable rows

As an example, make the rows selectable, and add a "select all" control too.

First you need something to select a row with: a checkbox. This page uses @kobalte/core, an accessible headless UI library, and lucide for icons. It shows how a headless UI library is used from Partas.Solid, since Kobalte is the main one with bindings.

Checkbox

Unstyled, a Kobalte checkbox is a root, a hidden input, a control and an indicator:

<Root class="size-8 bg-muted">
    <Input />
    <Control class="size-8">
        <Indicator />
    </Control>
</Root>

The Checkbox component inherits Kobalte.Checkbox:

open Partas.Solid.Kobalte

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

Add the implementation:

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

The checkbox needs a few accessibility parts inside it: Input, Control and Indicator. If you add them directly, you may hit a type error. Kobalte.Checkbox expects its child to be a function.

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)
            }
        }
    }
    }
Using the CheckboxRenderProp instead

The checkbox passes its state to its child function. Read indeterminate from there, and you no longer need to pass the property by name:

[<SolidTypeComponent>]
member props.checkbox =
    Kobalte.Checkbox(
        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)
            }
        }
    }
    }

Column definition

Add a column definition for the selection column, which holds the checkbox for each row.

This time, use the full signatures of ColumnDef's header and cell properties. They pass the row, or the table, 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
|]

Adding selection to the table

Change the table options to hold the selection state. First, make a signal for it:

[<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)

Then plug it into the TableOptions passed to createTable:

[<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?

Like data and columns, the row selection has to be a get property of the options object to be reactive. .stateFn hands you an object that makes that easy to do from Fable.

Render TestSelectableTable and try selecting rows.

Conclusion

That is the end of this exercise. The idea was to try some of the binding libraries and get familiar with Partas.Solid.

For data tables, @tanstack/table is one of the best known libraries around. Its API, and this large binding, are worth exploring.

Edit this page