// Fine-grained reactivity for F#
Reactive values.
Nothing else
recomputes.
Focused F# libraries for reactive Fable applications. Each library does one thing and can be used on its own.
open Fable.Ripple
type Todo = { Text: string; Done: bool }
let todos =
Var.create [
{ Text = "Write the guide"; Done = true }
{ Text = "Ship 1.0"; Done = false }
]
let remaining =
Signal.computed (fun () ->
todos.Value
|> List.filter (fun t -> not t.Done)
|> List.length
)
printfn "Remaining = %d" remaining.Value
todos.Value <-
todos.Value
|> List.map (fun t ->
if t.Text = "Ship 1.0" then
printfn "Completed: Ship 1.0"
{ t with Done = true }
else
t
)
printfn "Remaining = %d" remaining.Value
Synchronous
A write settles before the next line runs. No scheduler, no tick.
Glitch-free
A node runs once per write, after all its inputs are current.
Renderer-agnostic
The core knows nothing about the DOM. Rendering ships separately.
open Fable.Ripple
open Fable.Ripple.Dom
let counter () =
let count = Var.create 0
Html.div
[
Html.button
[
on.click (fun _ ->
count.Value <- count.Value + 1
)
Html.text "Count"
]
Html.output count
]
Html.mount "app" (counter ())
// Rendering
Signals, bound
straight to the DOM.
Fable.Ripple.Dom is an HTML DSL with no virtual tree. A signal knows which nodes read it, so a write updates those nodes and nothing else - no diff, no re-render of the component around them.
- One list model - attributes, events and children are items in the same list.
- Two-way bindings -
attr.bindValuekeeps an input and aVarin sync. - Control flow -
Html.showand keyedHtml.eachadd and remove real nodes.
// Routing
URLs parsed into
your own types.
Fable.UrlParser turns a URL into a value of your route type. Parsers compose left to right, in the order of the URL, and a failure reports the attempt that got furthest rather than a blank no-match.
- Typed segments -
string,int, and your own conversions. - Query and fragment - required, optional, repeated and flag parameters.
- Two-way codecs - one description parses a URL and builds it back.
open Fable.UrlParser
type Route =
| Home
| Blog of id: int
| Search of query: string * page: int option
let routes =
[
Parser.succeed Home
Parser.succeed (fun id -> Blog id)
|> Parser.segment "blog"
|> Parser.int
Parser.succeed (fun q page -> Search(q, page))
|> Parser.segment "search"
|> Parser.Query.Required.string "q"
|> Parser.Query.Optional.int "page"
]
[ "/blog/42"; "/search?q=signals&page=3"; "/blog/latest" ]
|> List.iter (fun url ->
Parser.tryParsePath routes url |> printfn "%s -> %A" url
)
// Forms
Typed forms,
one Var per field.
Fable.Ripple.Form composes fields into a form whose output is your own record. A parser turns each value into a typed result, and a parser that reads another field re-runs when that field changes. A keystroke writes one Var and touches no node.
- Combinators -
append,andThen,optional,showIf,list. - Validation - on submit, blur or change; async checks with debounce.
- Renderers - Bulma, plain classes, or in-place editing. Or write your own.
open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain
let email = Var.create ""
let password = Var.create ""
let repeat = Var.create ""
let form =
Form.succeed (fun email password _ -> email, password)
|> Form.append (
EmailField.create "email"
|> EmailField.withLabel "Email"
|> Field.create email (fun value ->
if value.Contains "@" then Ok value else Error "An email needs an @"
)
|> Form.emailField
)
|> Form.append (
PasswordField.create "password"
|> PasswordField.withLabel "Password"
|> Field.create password Ok
|> Form.passwordField
)
|> Form.append (
PasswordField.create "repeat"
|> PasswordField.withLabel "Repeat password"
|> Field.create repeat (fun value ->
if value = password.Value then Ok() else Error "The passwords do not match"
)
|> Form.passwordField
)
let state = Var.create View.Idle
Html.mount
"app"
(Form.View.asHtml
{
OnSubmit = fun (email, _) -> state.Value <- View.Success $"Welcome, %s{email}."
State = state
ErrorVisibility = View.errorVisibility ()
Action = View.Action.SubmitOnly "Sign up"
Validation = ValidateOnBlur
}
form)