Extension Methods
Extension methods cover what does not fit a typed property: arbitrary attributes, data-*, refs, style objects and spreads. Chain them after the constructor, before the children:
div(class' = "card").data("id", "42").attr("custom-attr", "yes") { "..." }
Write the call without a space between the constructor and the dot, as above. With a space, as in div (class' = "card").data(...), F# applies .data to the argument list instead of the tag.
| Method | On | Emits |
|---|---|---|
.attr(name, value) | #HtmlTag | name={value} |
.data(name, value) | #HtmlTag | data-name={value} |
.bool(name, value) | #HtmlTag | name={value} |
.ref(...) | tags | ref={...} |
.style'(...) | #HtmlTag | style={...} |
.class'(...) | #HtmlTag | class={...} |
.spread(value) | #HtmlElement | {...value} |
Changes in 3.0
| 2.x on Solid 1.9 | 3.0 on Solid 2 |
|---|---|
.on(event, handler) | Removed. Solid 2 dropped the on: namespace. Use the typed on* event properties. |
.prop(name, value) | Removed, with no replacement binding yet. |
.use'(directive) | Removed. Solid 2 dropped use: directives. Pass the directive to .ref as a callback. |
.classList(obj) | Renamed to .class'(obj), because Solid 2's class takes an object map. On HTML tags the extension is hidden by the class' property; see class' below. |
.bool emitted bool:name | .bool emits the plain attribute name. |
.spread was on #HtmlTag | .spread is on #HtmlElement, so components that only implement HtmlElement have it too. |
See Migrating to Solid 2 for the rest of the changes.
attr
Sets any attribute by name, including custom attributes and names F# does not allow as identifiers.
attr(name: string, value: obj)
data
Sets a data-* attribute. name is the part after data-.
data(name: string, value: string)
bool
Sets a boolean attribute by name. true adds the attribute and false removes it.
bool(name: string, value: bool)
div(style = "font-family: monospace").data("user-id", "42").attr("custom-attr", "yes").bool("itemscope", true) {
"Inspect this element to see its attributes."
}
export function Cell_c1() {
return <div style="font-family: monospace"
data-user-id="42"
custom-attr="yes"
itemscope={true}>
Inspect this element to see its attributes.
</div>;
}
ref
Gives you the DOM element once Solid has created it. .ref takes a variable, an option, a callback, an array of callbacks, a Ref or an array of Refs.
[<SolidComponent>]
let FocusDemo () =
let mutable inputRef: Browser.Types.HTMLInputElement = JS.undefined
div () {
input(placeholder = "Filled by the button").ref (inputRef)
button (onClick = fun _ -> inputRef.value <- "filled via ref"; inputRef.focus ()) { "Fill" }
}
export function FocusDemo() {
let inputRef = void 0;
return <div>
<input placeholder="Filled by the button"
ref={inputRef} />
<button onClick={(_arg) => {
inputRef.value = "filled via ref";
inputRef.focus();
}}>
Fill
</button>
</div>;
}
The same with a callback:
div().ref (fun (el: Browser.Types.HTMLDivElement) -> JS.console.log el) { "..." }
For HTML tags, the element type follows from the tag: input().ref takes an HTMLInputElement, div().ref an HTMLDivElement. If you have a more general type, cast it: div().ref (myRef :?> _).
The Ref<'T> union describes every shape a ref can take, so a component can take a ref prop and pass it on:
type Ref<'DomType> =
| Singleton of 'DomType option
| Callback of ('DomType -> unit)
| Array of Ref<'DomType> array
Ref.cast converts a Ref to another element type.
Refs and DOM libraries
The tooltip below drives Floating UI from npm through refs. Bind each export with [<Import>] and tupled parameters, so Fable emits a plain f(a, b, c) call:
[<Import("computePosition", "@floating-ui/dom")>]
let computePosition (reference: obj, floating: obj, options: obj): JS.Promise<obj> = jsNative
[<Import("autoUpdate", "@floating-ui/dom")>]
let autoUpdate (reference: obj, floating: obj, update: unit -> unit): (unit -> unit) = jsNative
[<Import("offset", "@floating-ui/dom")>]
let offset (distance: int): obj = jsNative
[<Import("flip", "@floating-ui/dom")>]
let flip (options: obj): obj = jsNative
[<Import("shift", "@floating-ui/dom")>]
let shift (options: obj): obj = jsNative
Both refs are set during render, so onSettled can pass the two elements to autoUpdate. autoUpdate returns a stop function, and returning that from onSettled runs it when the component is disposed. Anonymous records compile to plain option objects.
[<SolidComponent>]
let TooltipDemo () =
let mutable anchor: Browser.Types.HTMLButtonElement = JS.undefined
let mutable tip: Browser.Types.HTMLDivElement = JS.undefined
let place () =
// padding keeps the tooltip clear of the sticky navbar
let middleware = [| offset 10; flip {| padding = 64 |}; shift {| padding = 8 |} |]
computePosition(anchor, tip, {| placement = "top"; middleware = middleware |})
.``then``(fun pos ->
tip?style?left <- $"{pos?x}px"
tip?style?top <- $"{pos?y}px")
|> ignore
onSettled (fun () -> autoUpdate (anchor, tip, place))
let show visible = tip?style?opacity <- (if visible then "1" else "0")
let tipStyle =
"position: absolute; top: 0; left: 0; width: max-content; opacity: 0; "
+ "transition: opacity .15s; pointer-events: none; padding: .375rem .625rem; "
+ "border-radius: var(--nacara-radius-sm); font-size: .8125rem; "
+ "background: var(--nacara-heading); color: var(--nacara-bg)"
div (style = "display: flex; justify-content: center; padding: 3rem 0 1rem") {
button(
class' = "p-btn p-btn--secondary",
onMouseEnter = (fun _ -> show true),
onMouseLeave = (fun _ -> show false),
onFocus = (fun _ -> show true),
onBlur = (fun _ -> show false)
).attr("aria-describedby", "floating-tip").ref (anchor) { "Hover or focus me" }
div(style = tipStyle).attr("id", "floating-tip").attr("role", "tooltip").ref (tip) {
"Placed by Floating UI"
}
}
export function TooltipDemo() {
let anchor = void 0;
let tip = void 0;
onSettled(() => {
const clo = autoUpdate(anchor, tip, () => {
const middleware = [offset(10), flip({
padding: 64,
}), shift({
padding: 8,
})];
return computePosition(anchor, tip, {
middleware: middleware,
placement: "top",
}).then((pos) => {
tip.style.left = (`${pos.x}px`);
tip.style.top = (`${pos.y}px`);
});
});
return () => {
clo();
};
});
const show = (visible) => {
tip.style.opacity = (visible ? "1" : "0");
};
const tipStyle = "position: absolute; top: 0; left: 0; width: max-content; opacity: 0; transition: opacity .15s; pointer-events: none; padding: .375rem .625rem; border-radius: var(--nacara-radius-sm); font-size: .8125rem; background: var(--nacara-heading); color: var(--nacara-bg)";
return <div style="display: flex; justify-content: center; padding: 3rem 0 1rem">
<button class="p-btn p-btn--secondary"
onMouseEnter={(_arg) => {
show(true);
}}
onMouseLeave={(_arg_1) => {
show(false);
}}
onFocus={(_arg_2) => {
show(true);
}}
onBlur={(_arg_3) => {
show(false);
}}
aria-describedby="floating-tip"
ref={anchor}>
Hover or focus me
</button>
<div style={tipStyle}
id="floating-tip"
role="tooltip"
ref={tip}>
Placed by Floating UI
</div>
</div>;
}
Scroll until the button nears the top of the window. flip moves the tooltip below it, and autoUpdate keeps it attached while the page moves.
style'
Sets the style attribute from an object, or from a list of name and value pairs. The Style module in Partas.Solid.Style has typed helpers for the pairs.
style'(styleObj: obj)
style'(styles: (string * obj) list)
open Partas.Solid.Style
[<SolidComponent>]
let StyledBox () =
div().style' [ Style.backgroundColor Color.Lavender; "padding" ==> "1em"; "--accent" ==> "navy" ] {
span(style = "color: var(--accent)") { "Styled with a list, including a custom property." }
}
export function StyledBox() {
return <div style={{
"background-color": "lavender",
padding: "1em",
"--accent": "navy",
}}>
<span style="color: var(--accent)">
Styled with a list, including a custom property.
</span>
</div>;
}
A plain string works as well, through the style property: div (style = "color: blue").
class'
In Solid 2, class takes a string, an object map, or an array. The object map form replaces the old classList: each key whose value is true is added.
class'(classListObj: obj)
Pass the object through the property instead, with !! to convert it:
let isOpen, setOpen = createSignal true
div () {
button (onClick = fun _ -> setOpen (not (isOpen ()))) { "Toggle" }
span (class' = !!(createObj [ "is-open", box (isOpen ()); "closed", box (not (isOpen ())) ])) {
"Inspect my class"
}
}
spread
Spreads the properties of an object onto the element.
spread(value: obj)
[<SolidComponent>]
let SpreadLocal () =
let extra = createObj [ "id", box "spread-demo"; "title", box "from object"; "data-x", box "1" ]
div(style = "text-decoration: underline dotted").spread (extra) { "Hover me" }
export function SpreadLocal() {
const extra = {
id: "spread-demo",
title: "from object",
"data-x": "1",
};
return <div style="text-decoration: underline dotted"
{...extra} n$={false}>
Hover me
</div>;
}
In a SolidTypeComponent, .spread props spreads the props the body does not read (the PARTAS_OTHERS binding), passing unused attributes on to the rendered element:
namespace Partas.Solid.MyCenteredButton
open Partas.Solid
[<Erase>]
type MyCenteredButton() =
inherit button()
[<SolidTypeComponent>]
member props.View =
props.class' <- "centered"
div (style = "display: flex; justify-content: center") {
button(class' = props.class').spread props { props.children }
}
To spread every prop, reads included, see SpreadProps in Attribute Flags.
OnHandler
OnHandler is a record with handleEvent, once, passive and capture fields. It was the argument of the removed .on method, and nothing takes it now. To pass listener options in Solid 2, attach the listener yourself in a .ref callback with addEventListener.
