Skip to content

Storybook Support

The plugin ships a second attribute, PartasStorybook, that turns a storybook<'T> { ... } expression into a Storybook CSF module: a default-exported meta object and one named export per story.

open Partas.Solid
open Partas.Solid.Storybook

[<PartasStorybook>]
let private meta = storybook<Button> { () }

For setting up a Storybook project that picks up Fable's output, see the Storybook ecosystem page.

ArgTypes

The plugin infers argTypes from the component type. It reads each property's type and picks a control:

Property typeControlTable summary
boolbooleanbool
stringtextstring
[<StringEnum>] with casesradio under 6 options, else select[<StringEnum>]
chartextchar
RegextextRegex
integer and float typesnumber, with min/max/step set from the type's range (for example int and byte)the type name
tuples, lists, arrays, records, unions, anonymous recordsobjectthe type shape
functions, generic parametershiddenfunction, or GenericParam and the parameter's name
option, nullablethe wrapped type's control

To cut noise, properties inherited from the native tag bindings are ignored. They get no argType, and a value you give one in args is dropped too: the plugin only emits args for properties it collected. In the example below, btn.about <- "test" does not appear in the generated args.

[<StringEnum>] unions need extra help: see Cases. XML docs can steer generation: see XML docs.

Cases

A [<StringEnum>] union reaches the plugin as a plain string. There is no type information left to list its cases, so you declare them with the cases operation.

Take this component:

[<StringEnum>]
type Variant =
    | Black
    | Red
    | Blue

[<Erase>]
type Button() =
    interface RegularNode

    [<DefaultValue>]
    val mutable variant: Variant

By default variant gets a text control. To get the options, match on the property inside cases. Rider and most F# IDEs can generate the full match for you, and the values on the right-hand side do not matter:

[<PartasStorybook>]
let private meta =
    storybook<Button> {
        cases (fun btn ->
            match btn.variant with
            | Black -> failwith "todo"
            | Red -> failwith "todo"
            | Blue when btn.variant = Black -> failwith "todo"
            | _ -> ())
    }

The plugin now has every case, but not the union's name, since that was erased. The generated argType:

variant: {
    control: {
        type: "radio",
    },
    options: ["blue", "red", "black"],
    table: {
        type: {
            summary: "[<StringEnum>]",
        },
    },
},

XML docs

  • <summary> becomes the property's description.
  • <defaultValue> becomes the default value shown in the docs table.
  • <storybook /> is a closed tag whose attributes set options:
AttributeEffect
spy="true"On a function-typed property, sets the arg to fn() from storybook/test, so calls show in the Actions panel. Function properties whose names start with on get this without the attribute.
controlType="..."Overrides the control type, for example radio, select, color, date, range. Ignored on number properties. Experimental.
controlType="false"Hides the control. So does any value starting with hide.
/// <summary>Fired when the button is chosen.</summary>
/// <storybook spy="true"/>
[<Erase>]
member val choosey: Browser.Types.Event -> unit = unbox null with get, set

Args

args sets args for every story. The plugin reads them from the assignments in the lambda:

args (fun btn ->
    btn.variant <- Variant.Black)

Put a story name first to set args for that story alone. Each story name becomes a named export:

args "Default" (fun btn ->
    btn.variant <- Variant.Red)

Render

render sets the render function. Spread the props you are given into the component, or the controls do nothing:

render (fun props -> Button().spread props)

With a story name first, it sets the render function for that story:

render "Default" (fun props -> Button().spread props)

Decorators

decorator sets a decorator. Only one per story is supported for now. Call the story you are given:

decorator (fun story ->
    div () {
        "decorated:"
        story ()
    })

With a story name first, it applies to that story:

decorator "Default" (fun story ->
    div () {
        "default story decoration:"
        story ()
    })

Tags

tags sets the meta object's Storybook tags:

tags [| "autodocs" |]

Example

[<StringEnum>]
type Variant =
    | Brown2
    | Brown
    | Black

[<Erase>]
type Button() =
    interface RegularNode

    [<DefaultValue>]
    val mutable color: string

    [<DefaultValue>]
    val mutable variant: Variant

    /// <summary>
    /// Test
    /// </summary>
    [<Erase>]
    member val chocolate: string = unbox null with get, set

    /// <summary>
    /// sd
    /// </summary>
    /// <storybook spy="true"/>
    [<Erase>]
    member val choosey: Browser.Types.Event -> unit = unbox null with get, set

    [<SolidTypeComponent>]
    member props.__ =
        props.variant <- Variant.Black

        div (
            class' =
                match props.variant with
                | Brown -> "brown"
                | Black -> "black"
                | Brown2 -> "cho"
        )
[<PartasStorybook>]
let private meta =
    storybook<Button> {
        cases (fun btn ->
            match btn.variant with
            | Brown -> failwith "todo"
            | Black when btn.variant = Brown -> failwith "todo"
            | _ -> ())

        args (fun btn ->
            btn.variant <- Variant.Black
            btn.chocolate <- "Some value"
            btn.about <- "test")

        decorator (fun story -> div () { story () })
        decorator "brown" (fun story -> div () { div () { story () } })
        args "brown" (fun btn -> btn.variant <- Variant.Brown)
        render (fun btn -> Button(about = "test").spread btn { "Test" })
        render "brown" (fun btn -> Button(about = "brown").spread btn { "brown test" })
        args "choc" (fun btn -> ())
        render "choc" (fun btn -> Button())
    }

This output was captured with Partas.Solid 2.x, with the spread marker updated to 3.0's n$={false}:

const meta = {
    args: {
        chocolate: "Some value",
        choosey: fn(),
        variant: "black",
    },
    argTypes: {
        color: {
            control: {
                type: "text",
            },
            table: {
                type: {
                    summary: "string",
                },
            },
        },
        chocolate: {
            control: {
                type: "text",
            },
            description: "Test",
            table: {
                type: {
                    summary: "string",
                },
            },
        },
        choosey: {
            control: {
                type: false,
            },
            description: "sd",
            table: {
                type: {
                    summary: "function",
                },
            },
        },
        variant: {
            control: {
                type: "radio",
            },
            options: ["brown", "black"],
            table: {
                type: {
                    summary: "[<StringEnum>]",
                },
            },
        },
    },
    decorators: [(story) => <div>
        {story()}
    </div>],
    render: (btn_3) => <Button about="test"
        {...btn_3} n$={false}>
        Test
    </Button>,
    component: Button_1
 };

export default meta;

export const brown = {
        args: {
            variant: "brown",
        },
        render: (PARTAS_RENDER_BUILDER) => <Button about="brown"
            {...PARTAS_RENDER_BUILDER} n$={false}>
            brown test
        </Button>,
        decorators: [(PARTAS_DECORATOR_BUILDER) => <div>
            <div>
                {PARTAS_DECORATOR_BUILDER()}
            </div>
        </div>],
    }
export const choc = {
        args: {},
        render: (PARTAS_RENDER_BUILDER_2) => <Button />,
    }

const $PARTAS_DISCARD = { $discard: true,
};
Edit this page