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 type | Control | Table summary |
|---|---|---|
bool | boolean | bool |
string | text | string |
[<StringEnum>] with cases | radio under 6 options, else select | [<StringEnum>] |
char | text | char |
Regex | text | Regex |
| integer and float types | number, with min/max/step set from the type's range (for example int and byte) | the type name |
| tuples, lists, arrays, records, unions, anonymous records | object | the type shape |
| functions, generic parameters | hidden | function, or GenericParam and the parameter's name |
| option, nullable | the 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:
| Attribute | Effect |
|---|---|
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,
};
