Fable.Lit is a collection of tools to help you write Fable apps by embedding HTML code into your F# code with the power of lit-html. Thanks to this, you can use HTML from a designer or a component library right away, without any kind of conversion to F#. lit-html only weighs 3KB minified and gzipped so it's very cheap to integrate in your existing app (see below for React integration). And if you're using VS Code and install the Highlight HTML/SQL templates in F# extension the integration will be even smoother:
There's an example in the sample
directory, but you can find here a more detailed tutorial by Angel Munoz.
Fable.Lit packages require lit-html 2 from npm and fable 3.3 dotnet tool which are both, at the time of writing, in prerelease state.
npm install html-lit@next
dotnet tool install fable --version 3.3.0-beta-002
Then, in the directory of your .fsproj, install the packages you need (see below for details), which are also in prelease. Note the package ids are prefixed by Fable.
but not the actual namespaces.
dotnet add package Fable.Lit --prerelease
dotnet add package Fable.Lit.React --prerelease
dotnet add package Fable.Lit.Elmish --prerelease
dotnet add package Fable.Lit.Feliz --prerelease
Fable.Lit contains bindings and extra helpers for lit-html. Please read lit-html documentation to learn how Lit templates work.
When you open the Lit
namespace, you will have access to:
html
andsvg
helpers, which convert F# interpolated strings into lit-html templatesLitHtml
static class containing raw bindings for lit-html (normally you don't need to use this)Lit
static class containing wrappers for lit-html in a more F# idiomatic fashion
E.g. lit-html repeat directive becomes Lit.mapUnique
to map a sequence of items into Lit.TemplateResult
and assign each a unique id. This is important to identify the items when the list is going to be sorted or filtered. For static lists passing the sequence directly just works.
let renderList items =
let renderItem item =
html $"""<li>Value: <strong>{item.Value}</strong></li>"""
html $"""<ul>{items |> Lit.mapUnique (fun x -> x.Id) renderItem}</ul>"""
Fable.Lit includes the HookComponent
attribute. When you decorate a view function with it, this lets you use hooks in a similar way as ReactComponent attribute does. Hook support is included in Fable.Lit's F# code and doesn't require any extra JS dependency besides lit-html.
[<HookComponent>]
let NameInput() =
// Lit.Hook API is currently evolving, we try to emulate React's API but there may be some differences
let value, setValue = Hook.useState "World"
let inputRef = Hook.useRef<HTMLInputElement>()
html $"""
<div class="content">
<p>Hello {value}!</p>
<input
value={value}
{Lit.refValue inputRef}
@focus={fun _ ->
inputRef.value |> Option.iter (fun el -> el.select())}
@keyup={fun (ev: Event) ->
ev.target.Value |> setValue}>
</div>
"""
Note that hook components are just a way to keep state between renders and are not web components. We plan to add bindings to define web components with lit in the near future. Also check Fable.Haunted by Angel Munoz to define actual web components with React-style hooks.
Thanks to the great work by Cody Johnson with Feliz.UsElmish, Fable.Lit HookComponents also include useElmish
hook to manage the internal state of your components using the model-view-update architecture.
open Elmish
open Lit
type Model = ..
type Msg = ..
let init() = ..
let update msg model = ..
let view model dispatch = ..
[<HookComponent>]
let Clock(): TemplateResult =
let model, dispatch = Hook.useElmish(init, update)
view model dispatch
Fable.Lit.React package contains helpers to integrate lit-html with React in both directions: either by rendering a React component with an HTML template or by embedding a React component in an HTML template. This makes it possible to add raw HTML to your apps whenever you need it, no matter you're using Fable.React bindings or Zaid Ajaj's Feliz API.
If you're comfortable with JSX and Typescript/JS, it's also easy to invoke them from Feliz if that suits your needs better.
Use React.lit_html
(or svg) to include the string template directly. Or transform an already-compiled template with React.ofLit: Lit.TemplateResult -> ReactElement
. These helpers use hooks so they must be called directly in the root of a React component.
[<ReactComponent>]
let Clock () =
let time, setTime = React.useState DateTime.Now
React.useEffectOnce(fun () ->
let id = JS.setInterval (fun _ -> DateTime.Now |> setTime) 1000
React.createDisposable(fun () ->
JS.clearInterval id))
// If the template were in another function we would call
// view time |> React.ofLit
React.lit_html $"""
<svg viewBox="0 0 100 100"
width="350px">
<circle
cx="50"
cy="50"
r="45"
fill="#0B79CE"></circle>
{clockHand time.AsHour}
{clockHand time.AsMinute}
{clockHand time.AsSecond}
<circle
cx="50"
cy="50"
r="3"
fill="#0B79CE"
stroke="#023963"
stroke-width="1">
</circle>
</svg>
"""
Use React.toLit
to transform a React component into a lit-html renderer function. Store the transformed function in a static value to make sure a new React component is not instantiated for every render:
module ReactLib =
open Fable.React
open Fable.React.Props
[<ReactComponent>]
let MyComponent showClock =
let state = Hooks.useState 0
div [ Class "card" ] [
div [ Class "card-content" ] [
div [ Class "content" ] [
p [] [str $"""I'm a React component. Clock is {if showClock then "visible" else "hidden"}"""]
button [
Class "button"
OnClick (fun _ -> state.update(state.current + 1))
] [ str $"""Clicked {state.current} time{if state.current = 1 then "" else "s"}!"""]
]
]
]
open Lit
let ReactLitComponent =
React.toLit ReactLib.MyComponent
// Now you can embed the React component into your lit-html template
let view model dispatch =
html $"""
<div class="vertical-container">
{ReactLitComponent model.ShowClock}
{if model.ShowClock then Clock.Clock() else Lit.nothing}
</div>
"""
Fable.Lit.Elmish allows you to write a frontend app using the popular Elmish library by Eugene Tolmachev with a view function returning Lit.TemplateResult
. The package also includes support for Hot Module Replacement out-of-the-box thanks to Maxime Mangel original work with Elmish.HMR.
open Elmish
open Lit
type Model = ..
type Msg = ..
let init() = ..
let update msg model = ..
let view model dispatch = ..
open Lit.Elmish
open Lit.Elmish.HMR
Program.mkProgram initialState update view
|> Program.withLit "app-container"
|> Program.run
Fable.Lit.Feliz package makes it possible to build lit-html templates in a type-safe manner using Feliz.Engine (check here the differences between original Feliz and Feliz.Engine APIs).
let buttonFeliz (model: Model) dispatch =
Feliz.toLit <| Html.button [
Attr.className "button"
Ev.onClick (fun _ -> ToggleClock |> dispatch)
Html.text (if model.ShowClock then "Hide clock" else "Show clock")
]
The only thing you need to take into account is templates are built once and then cached (this is necessary because of the way lit-html 2 templates work), so you cannot change the structure of a template dynamically with conditions. This doesn't mean that templates need to be static forever, but you need to "compile" (that is, convert with Feliz.toLit: Lit.Feliz.Node -> Lit.TemplateResult
) any nested structure that is bound to change. The exception to this are CSS styles and single text nodes, which are considered values and not structure, so they can change dynamically.
let buttonFeliz (model: Model) dispatch =
Feliz.toLit <| Html.button [
Attr.className "button"
Ev.onClick (fun _ -> ToggleClock |> dispatch)
// This doesn't work
// if model.ShowClock then Html.text "Hide clock"
// else Html.strong "Show clock"
// Do this instead
Feliz.ofLit <|
if model.ShowClock then Lit.ofText "Hide clock"
else Feliz.toLit <| Html.strong "Show clock"
// Alternatively you can just embed a Lit template in a Feliz node
// if model.ShowClock then Html.text "Hide clock"
// else Feliz.lit_html $"""<strong>Show clock</strong>"""
]
This is actually the same you would do with HTML templates, as you need to put conditions in a hole of the interpolated strings and declare the nested template externally.
let buttonLit (model: Model) dispatch =
let strong txt =
html $"<strong>{txt}</strong>"
html $"""
<button class="button"
@click={fun _ -> ToggleCock |> dispatch}>
{if model.ShowClock then Lit.ofText "Hide Clock" else strong "Show Clock"}
</button>
"""
Even if you prefer to use HTML templates, Lit.Feliz contains a useful helper to declare inline styles with a nicely typed and self-discoverable API:
module Styles =
let verticalContainer = [
Css.marginLeft(rem 2)
Css.displayFlex
Css.justifyContentCenter
Css.alignItemsCenter
Css.flexDirectionColumn
]
let view model dispatch =
html $"""
<div style={Feliz.styles Styles.verticalContainer}>
...
</div>
"""