-
Notifications
You must be signed in to change notification settings - Fork 55
HTML
Bolero provides F# functions to create HTML elements, attributes and event handlers, and also instantiate Blazor components. All of these are defined in the module Bolero.Html.
See also how to create HTML elements using HTML templates.
To create an HTML element, just call the function with its name. It takes two arguments: a list of attributes and a list of child elements, and returns a value of type Node.
Additionally, the function text creates a text node, and textf creates a text node using printf-style formatting.
let myElement name =
div [] [
h1 [] [text "My app"]
p [] [textf "Hello %s and welcome to my app!" name]
]Elements that can't have children, such as input or br, only take attributes as argument.
let myElement =
p [] [
text "First line of the paragraph."
br []
text "Second line of the paragraph."
]To create a custom element for which there isn't a function, use elt.
let myElement =
elt "data-paragraph" [] [
text "This is in a <data-paragraph> element."
]In addition to representing an HTML node, the type Node can also represent a (possibly empty) sequence of nodes. This is done using the concat function.
let myElements =
concat [
p [text "First paragraph"]
p [text "Second paragraph"]
]empty represents an empty sequence of nodes: it is equivalent to concat []. This doesn't seem very useful at first, but it is actually important for conditional elements.
Due to the way that Blazor compares the rendered DOM when a change is applied, the returned HTML must always have the same structure: conditional elements can't be simply added. For example, the following may cause runtime errors:
// May fail at runtime.
let myButton (label: option<string>) =
button [] [
if label.IsSome then
yield text label.Value
]Rendering such conditional content must be done with the cond function instead.
-
condcan take a boolean value, and a function to call on this value returning a Node. For example, the following is correct:let myButton (label: option<string>) = button [] [ cond label.IsSome <| function | true -> text label.Value | false -> empty ]
You can also see here why
emptyis a useful value. -
condcan also take a value whose type is an F# union, and a function that matches over the cases of this union. For example,option<'T>is an F# union, so the following is correct:let myButton (label: option<string>) = button [] [ cond label <| function | Some l -> text l | None -> empty ]
Here's an example with a union defined in your code:
/// A list of usernames, truncated to two + number of others type UserList = | One of string | Two of string * string | Many of string * string * int /// Shows one of the following, depending on the number of users: /// * "*Alice* likes this" /// * "*Alice* and *Bob* like this" /// * "*Alice*, *Bob* and 12 others like this" let showLikes (users: UserList) = concat [ cond users <| function | One uname -> b [] [text uname] | Two (uname1, uname2) -> concat [ b [] [text uname1] text " and " b [] [text uname2] ] | Many (uname1, uname2, others) -> concat [ b [] [text uname1] text ", " b [] [text uname2] textf " and %i others" others ] cond users <| function | One _ -> text " likes this." | _ -> text " like this." ]
Similarly, rendering collections using a function such as List.map to create a list of nodes can cause runtime errors. Instead, collections of items should be rendered using the function forEach.
let listUsers (names: string list) =
p [] [
text "Here are the users:"
ul [] [
forEach names <| fun name ->
li [] [text name]
]
]Attributes are available in the attr submodule.
let myElement =
p [
attr.style "color: blue;"
attr.``class`` "paragraph"
] [
text "Hello and welcome to my app!"
]To create a custom attribute for which there isn't a function, use the => operator.
let myElement =
p ["data-kind" => "paragraph"] [
text "Hello and welcome to my app!"
]Like with elements (see Conditional elements), naively adding conditional attributes can lead to runtime errors.
// May fail at runtime.
let myElement (isBlue: bool) =
p [
if isBlue then
yield attr.style "color: blue;"
] [
text "Hello and welcome to my app!"
]Instead if an attribute may or may not need to be added depending on a condition, always add the attribute and give it a value of false or null when it should be omitted.
let myElement (isBlue: bool) =
p [attr.style (if isBlue then "color: blue;" else null)] [
text "Hello and welcome to my app!"
]Event handlers are available in the on submodule.
let myElement =
button [on.click (fun _ -> printfn "Clicked!")] [
text "Click me!"
]The argument passed to the callback has type UIEventArgs from Blazor. Specific events have corresponding subtypes of UIEventArgs: for example, on.click uses UIMouseEventArgs.
let myElement =
button [
on.click (fun e ->
printfn "Clicked at (%i, %i)" e.ClientX e.ClientY)
] [
text "Click me!"
]To create a custom event handler for which there isn't a function, use on.event.
let myElement =
button [
on.event "customevent" (fun _ -> printfn "Custom event!")
] [
text "Click me!"
]Attributes defined in the bind module define two-way binding with the element's value. These functions take two arguments:
- The current value, which generally comes from the Elmish model.
- A setter function, which generally calls the Elmish dispatch function.
Here is an example using bind.input:
type Model = { username: string }
type Message =
| SetUsername of string
let hello model dispatch =
input [
bind.input model.username (fun n -> dispatch (SetUsername n))
]The functions in the bind module are:
-
bind.inputbinds to thevalueproperty of an element, as a string, by listening to theoninputevent. This means that the callback is called on every user interaction on the element that changes its value. For example, on a text input, it is triggered on every keystroke.It is suitable for
inputandtextareaelements. -
bind.changeis identical tobind.inputexcept that it listens to theonchangeevent. This means that the callback is called when a change is "committed" by the user. For example, on a text input, it is triggered when the user presses Enter or unfocuses the element after changing the value.It is suitable for
input,textareaandselectelements. -
bind.inputIntandbind.changeIntbind to thevalueproperty of an element as anint, by listening to the corresponding event. They are particularly suitable with an input that hasattr.``type`` "number". If the value cannot be parsed as anintduring an event, the setter is not called. -
bind.inputFloatandbind.changeFloatbind to thevalueproperty of an element as afloat, by listening to the corresponding event. They are particularly suitable with an input that hasattr.``type`` "number". If the value cannot be parsed as afloatduring an event, the setter is not called. -
bind.checkedbinds to thecheckedproperty of a checkbox input. Note that you also need to addattr.``type`` "checkbox"to the input.
Note: Radio buttons (
attr.``type`` "radio") are not yet supported by Blazor, and therefore by Bolero; see the issue on Blazor's tracker.
type Model = { isChecked: bool }
type Message =
| SetChecked of bool
let hello model dispatch =
input [
attr.``type`` "checkbox"
bind.checked model.isChecked (fun c -> dispatch (SetChecked c))
]Note: this section describes how to create and use plain Blazor components. It is recommended to use Elmish components whenever possible; see Using Elmish.
You can create plain Blazor components by inheriting from the Component type.
type MyComponent() =
inherit Component()
override this.Render() =
div [] [text "Hello, world!"]To add parameters to the component, use a property with the Parameter attribute from namespace Microsoft.AspNetCore.Blazor.
type MyComponent() =
inherit Component()
[<Parameter>]
member val Who = "" with get, set
override this.Render() =
div [] [text (sprintf "Hello, %s!" this.Who)]To instantiate a Blazor component, use the comp function. It is parameterized by the component type, and takes attributes and child nodes as arguments.
let myElement =
comp<MyComponent> ["Who" => "world"] []The function navLink is a helper to create a Blazor NavLink component. This component creates an <a> tag which dynamically receives the "active" CSS class whenever the current page URL matches its own href. The match is customized by passing NavLinkMatch.All (to only match the full URL path) or NavLinkMatch.Prefix (to match any URL that starts with the navLink's href).
let myMenu =
ul [] [
li [] [navLink NavLinkMatch.All [attr.href "/"] [text "Home"]]
li [] [navLink NavLinkMatch.Prefix [attr.href "/blog"] [text "Blog"]]
]