Polymorphism
Libraries such as Kobalte and ArkUI let a component render as another component or tag, passing its props on. In Kobalte this is the as prop:
<Button as={a} href={"/"} />
The plugin supports this for Kobalte and ArkUI, and lets you enable it for other libraries. The F# side lives in the auto-opened Partas.Solid.Polymorphism module.
Making a component polymorphic
A component opts in by implementing the Polymorph interface. That gives it the .as' extension method, which takes either a built element or a TagValue. Pass an element when you can: F# cannot inherit the morph target's props dynamically, but it does type-check them in the target's own constructor.
Here Box renders whatever its as prop holds through Dynamic, and defaults to a div:
open Partas.Solid.Web
[<Erase>]
type Box() =
inherit div()
interface Polymorph
[<Erase>]
member val ``as``: obj = unbox null with get, set
[<SolidTypeComponent>]
member props.View =
props.``as`` <- box "div"
Dynamic<obj>(component' = unbox props.``as``).spread props
[<SolidComponent>]
let BoxAsButton () =
let count, setCount = createSignal 0
Box(class' = "boxed")
.as'(button (type' = "button", onClick = fun _ -> setCount (count () + 1))) {
$"Pressed {count ()} times"
}
export function Box(props) {
props = merge({
as: "div",
}, props);
const PARTAS_OTHERS = omit(props, "as");
return <Dynamic component={props.as}
{...PARTAS_OTHERS} n$={false} />;
}
export function BoxAsButton() {
const patternInput = createSignal(0);
const count = patternInput[0];
return <Box class="boxed"
as={(PARTAS_POLYPROPS) => <button {...PARTAS_POLYPROPS} n$={false}
type="button"
onClick={(_arg) => {
patternInput[1](count() + 1);
}} />}>
{`Pressed ${count()} times`}
</Box>;
}
The element passed to .as' becomes a function that spreads the parent's props onto the morph target:
as={(PARTAS_POLYPROPS) => <button {...PARTAS_POLYPROPS} n$={false} type="button" onClick={...} />}
So the rendered <button> gets Box's class and children, and keeps the type and onClick set on it.
The morph target can be another component. Its own props arrive alongside the parent's:
[<Erase>]
type ToneLink() =
inherit a()
[<Erase>]
member val tone: string = unbox null with get, set
[<SolidTypeComponent>]
member props.View =
a(style = (if props.tone = "loud" then "font-weight: bold" else "")).spread props { props.children }
[<SolidComponent>]
let BoxAsLink () =
Box(title = "Go to the top").as'(ToneLink(tone = "loud", href = "#")) { "A Box rendered as a ToneLink" }
export function ToneLink(props) {
const PARTAS_OTHERS = omit(props, "tone", "children");
return <a style={(props.tone === "loud") ? "font-weight: bold" : ""}
{...PARTAS_OTHERS} n$={false}>
{props.children}
</a>;
}
export function BoxAsLink() {
return <Box title="Go to the top"
as={(PARTAS_POLYPROPS) => <ToneLink {...PARTAS_POLYPROPS} n$={false}
tone="loud"
href="#" />}>
A Box rendered as a ToneLink
</Box>;
}
A TagValue passed to .as' goes through unchanged, so the morph target's props are untyped and set with .attr.
Polymorphic attributes
The plugin treats these extension methods as polymorphic attributes:
| Method | JSX prop | For |
|---|---|---|
.as' | as | Kobalte |
.asChild | asChild | ArkUI |
any name starting __PARTAS_POLYMORPHIC__ | the name with the prefix cut off | anything else |
Only .as' comes with Partas.Solid. The plugin recognises asChild, but you declare that extension yourself, the same way as a custom one below. This is the built-in declaration:
[<Erase; Extension>]
type PolymorphicExtensions =
[<Erase; Extension>]
static member as'<'Base when 'Base :> Polymorph>(this: 'Base, morph: #HtmlTag) : 'Base = this
[<Erase; Extension>]
static member as'<'Base when 'Base :> Polymorph>(this: 'Base, morph: TagValue) : 'Base = this
Custom polymorphic attributes
For a library whose polymorphic prop has another name, declare an extension method with the magic prefix. This one makes render polymorphic:
module MyLibrary.PolymorphicExt
open Partas.Solid
open Fable.Core
[<Erase; System.Runtime.CompilerServices.Extension>]
type CustomPolymorphicExtensions =
[<Erase; System.Runtime.CompilerServices.Extension>]
static member __PARTAS_POLYMORPHIC__render<'Base when 'Base :> Polymorph>(this: 'Base, morph: #HtmlTag) : 'Base = this
Slot(id = "slot").__PARTAS_POLYMORPHIC__render(article (class' = "art")) { "slotted" }
The plugin cuts the prefix off the name, turns the element into a function as above, and emits render={(PARTAS_POLYPROPS) => <article ... />}.
.attr
Without any of this, you can pass a tag value to the library's polymorphic prop and chain .attr for the props the parent does not have, at the cost of type checking.
