Components
SolidA component in OCaml's JSX, its props, its children, and how the compiler checks them.
A component is a function from its props to an element, as in Solid. It
runs once; what changes afterwards is the thunks inside it, which Solid
reruns. Every module of solidtype binds one part of Solid, named as Solid
names it: Solidtype.Solid is solid-js, Solidtype.Web is
solid-js/web, Solidtype.Show is <Show>.
let%component
Section titled “let%component”A component another component’s JSX names is written with
let%component. Its props are its labelled parameters, and it ends in
():
Hello, Ada!
Welcome, Grace!
module Jsx = Solidtype.Jsx
module Greeting = struct let%component make ~(name : string) ?(greeting : string = "Hello") () = <p>(fun () -> Jsx.string (greeting () ^ ", " ^ name () ^ "!"))</p>end
let make () = <div class_="flex flex-col gap-1"> <Greeting name="Ada" /> <Greeting name="Grace" greeting="Welcome" /> </div>Greeting is written with let%component; the make at the bottom is not.
The difference is who calls them.
The bottom make is a root. Nothing passes it props: a page mounts it, or
Web.render renders it, and it is an ordinary OCaml function, let make () = .... No JSX tag names it.
Greeting is named by a tag, <Greeting name="Ada" />, and Solid calls
it the way it calls every component: with one JavaScript object of props,
{ name: "Ada" }, not with OCaml’s labelled arguments. Each field of that
object is a getter that Solid tracks when it is read, so a prop whose value
changes is seen by the code that reads it then – which is why, in TSX,
destructuring props in the parameters loses updates. A plain let make ~name () = ... could not be given that object, and reading name once at
the top would be that same lost update.
let%component bridges the two. From the labelled parameters, the ppx
writes:
Props, the module that describes the props object: its type, a getter for each field,check, which the compiler applies to every<Greeting ... />to catch a prop left out or misspelt, andomitandothers, for a spread;make, a function of that object, in which each parameter becomes a read of its field:name ()readsprops.nameat the moment it runs, inside a thunk, where Solid tracks it;component,makeas the value a JSX tag names.
So in the body a value prop is read as a call, name (), never as a plain
value. An optional parameter with a default,
?(greeting : string = "Hello"), reads its default while the prop is left
out; one without, ?(note : string option), reads an option.
Written by hand, Greeting would be its Props module, a getter and a
check per prop, kept in step with the body by a reader. let%component
writes them from the one place the props are named, so they cannot drift.
A component is a value, Greeting.component, and the tag <Greeting />
names it. The module is the component: <Greeting.Header /> is the
component Greeting.Header.component.
Props are checked
Section titled “Props are checked”Each element of a component applies the component’s Props.check to its
props, which the compiler checks and Melange drops. The check takes one
labelled argument per prop, optional where the prop is, so the compiler
says what TSX’s types would:
1 | let make () = <Label /> ^^^^^^^^^Error: This expression has type text:string Solidtype.Check.value -> unit but an expression was expected of type unitHint: This function application is partial, maybe some arguments are missing.1 | let make () = <Label text=(fun () -> "x") colour="red" /> ^^^^^Error: The function applied to this argument has type ?count:int Solidtype.Check.value -> ?props:Label.Props.t -> unitThis argument cannot be applied with label ~colourA value prop is a Check.value: a literal, or a thunk that reads what
changes. A thunk is never taken for a function prop, which a component
would call; a function prop is the function itself, on_close=(fun _ -> ...). Two props typed by one type variable must agree, as in TSX.
Children
Section titled “Children”Children are a prop, children. As Jsx.content, they are any JSX and are
read where they go, (fun () -> children ()):
A panel's children are any JSX: these two paragraphs.
Close it, and a button takes its place.
module S = Solidtype.Solidmodule Jsx = Solidtype.Jsxmodule Show = Solidtype.Show
module Panel = struct let%component make ~(title : string) ~(on_close : unit -> unit) ~(children : Jsx.content) () = <section class_="flex w-72 flex-col gap-2 rounded-xl border border-border p-4"> <header class_="flex items-center justify-between"> <strong>(fun () -> Jsx.string (title ()))</strong> <button class_="button button--sm button--tertiary" type_=`button on_click=(fun _ -> on_close ())> "Close" </button> </header> (fun () -> children ()) </section>end
let make () = let is_open, set_open = S.create_signal true in <Show when_=(fun () -> S.get is_open) fallback=(<button class_="button button--sm button--secondary" type_=`button on_click=(fun _ -> S.set set_open true)> "Open the notes" </button>)> <Panel title="Notes" on_close=(fun _ -> S.set set_open false)> <p>"A panel's children are any JSX: these two paragraphs."</p> <p class_="text-sm text-muted"> "Close it, and a button takes its place." </p> </Panel> </Show>A component may also take a function as its only child, which it calls, as
For does: ~(children : item -> Jsx.element). A check names which kinds
it takes: [ nodes ]for elements,[ render ] for a function, or both.
A function prop that takes nothing is written (fun _ -> ...), as
on_close is above, which hides the panel and shows a button in its place: (fun () -> ...) is a thunk, which would be read as a
value.
Props written by hand
Section titled “Props written by hand”Besides a root, one kind of component is written without
let%component: a component
that passes its props on whole, as a spread, writes its Props by hand,
its check’s labels named as solidtype names a prop, and spreads them with
the attribute props:
let make (props : Props.t) = <input class_="field" props />Solid.merge_props gives props defaults, live, and Props.omit, which
let%component writes, leaves some out of a spread by a variant of the
component’s own prop names, so a misspelt one is a compile error.
Props.others leaves out all of them: what is left is what the
component’s check admits besides them, its data- attributes, which it
passes on to its element, props=(Props.others props). A component’s
spread is a rest of its own props, or that rest of any component’s.
A component chosen at run time
Section titled “A component chosen at run time”Dynamic renders whichever component a thunk gives, its props given as one
value typed by that component, so a prop it needs is not left out:
<Dynamic component=(fun () -> pick ()) props=(Greeting.Props.make ~name:"Ada" ()) />Dynamic.Element does the same for an element whose tag is a string.
Solid.lazy_ makes a component loaded when it is first rendered, which
suspends until it arrives.
What else is bound
Section titled “What else is bound”| Solid | solidtype |
|---|---|
children(() => props.children) |
Solid.children, then Solid.to_array |
mergeProps |
Solid.merge_props |
splitProps |
Solid.omit and Solid.others, or Props.omit and Props.others |
createUniqueId |
Solid.create_unique_id |
lazy |
Solid.lazy_ |
<Dynamic> |
Solidtype.Dynamic, Dynamic.Element |
<Portal> |
Solidtype.Portal |
Each module’s .mli is its reference, and says where OCaml writes a form
Solid’s TypeScript has differently.
