Using Blazor features

Blazor Components and their interoperability

Components

Creating a component

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 { "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 { $"Hello, {this.Who}!" }

Using a component

This section documents how to use a Blazor Component, either referenced from a C# Razor project, or created in F# by inheriting from Component.

To instantiate a Blazor component, use the comp computation expression builder. It is parameterized by the component type, and takes attributes and child nodes in its CE body. To set a parameter, pass it by name as an attribute using the => operator. If there are no parameters to pass, use attr.empty().

let myElement : Node =
    comp<MyComponent> { "Who" => "world" }

let myElementWithDefaultWho : Node =
    comp<MyComponent> { attr.empty() }

When inserting a component without parameters inside a parent element, { attr.empty() } can be omitted:

let myComponentInAnElement : Node =
    div {
        attr.id "greeting"
        comp<MyComponent>
    }

Additionally, some parameter types must be handled specially:

  • Parameters of type EventCallback<T> can be passed using one of these functions:

    • attr.callback, which takes a function T -> unit;
    • attr.async.callback, which takes a function T -> Async<unit>;
    • attr.task.callback, which takes a function T -> Task.

    Here is an example using the library MatBlazor:

    open MatBlazor
    
    let myButton model dispatch =
        comp<MatButton> {
          attr.callback "OnClick" (fun _ -> dispatch ButtonClicked)
          "Click me!"
        }
    

    In Blazor, these parameters would be passed as Action<T>.

  • Parameters of type RenderFragment and RenderFragment<T> can be passed using the function attr.fragment and attr.fragmentWith, respectively. The former takes a Node, and the latter a function T -> Node.

    Here is again an example using MatBlazor:

    open MatBlazor
    
    type Car =
      { Name: string
        Price: decimal
        Horsepower: int }
    
    type Model =
      { Cars: Car[] }
    
    let myTable model dispatch =
        comp<MatTable> {
            "Items" => model.Cars
    
            attr.fragment "MatTableHeader" (
                concat {
                    th { "Name" }
                    th { "Price" }
                    th { "Horsepower" }
                }
            )
    
            attr.fragmentWith "MatTableRow" (fun (car: Car) ->
                concat {
                    td { car.Name } 
                    td { $"%.2f{car.Price}" }
                    td { $"{car.Horsepower}" }
                }
            )
        }
    

Cascading

You can use Cascading Values and Cascading Parameters as well

let view state dispatch = 
   comp<CascadingValue<int>> { "Value" => 42; "Name" => "MeaningOfLife"; body }

in this case body is the content of your view somewhere down the hierarchy you may have something like this

type MyProgramComponent() =
    inherit ProgramComponent<State, Msg>()

    [<CascadingParameter(Name = "MeaningOfLife")>]
    member val MeaningofLife: int = 0 with get, set
    
    override this.Program =
        Program.mkProgram init update view
        |> Program.runWith this.MeaningOfLife

or a blazor component as well

type FooComponent() =
    inherit Component()

    [<CascadingParameter>]
    member val MeaningOfLife = 0 with get, set

    override this.Render() =
        concat {
            h1 { "Foo component" }
            p { "The meaning of life is {this.MeaningOfLife}" }
        }

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 "/"; "Home" } }
        li { navLink NavLinkMatch.Prefix { attr.href "/blog"; "Blog" } }
    }

This page documents some useful features of Blazor and how to best use them from a Bolero application.

Dependency injection

Blazor provides the ability to work with the dependency injection mechanism introduced by ASP.NET Core. Learn more about it on the official documentation.

Requiring a dependency

Any Blazor component can require a dependency. This includes the Bolero ProgramComponent, as well as any Component class you create. This is done by creating a mutable property with the attribute Microsoft.AspNetCore.Components.Inject:

open Microsoft.AspNetCore.Components
open Bolero

type MyApp() =
    inherit ProgramComponent<Model, Message>()

    [<Inject>]
    member val MyDependency = Unchecked.defaultof<IMyDependency> with get, set
    
    override this.Program =
        doSomethingWith this.MyDependency

Providing a dependency

Dependencies are injected in the client-side host builder:

module Program =

    [<EntryPoint>]
    let Main args =
        let builder = WebAssemblyHostBuilder.CreateDefault(args)

        builder.Services.AddSingleton<IMyDependency, MyDependencyImpl>() |> ignore

        builder.RootComponents.Add<MyApp>("#main")
        builder.Build().RunAsync() |> ignore
        0

JavaScript interoperability

It is possible to use Blazor's JavaScript interoperability interface IJSRuntime in Bolero using dependency injection. Learn more about it on the official documentation.

open Microsoft.JSInterop

type MyComponent() =
    inherit ElmishComponent<Model, Message>()

    [<Inject>]
    member val JSRuntime = Unchecked.defaultof<IJSRuntime> with get, set

    override this.View model dispatch =
        button {
            on.task.click (fun _ ->
                this.JSRuntime.InvokeVoidAsync("console.log", model).AsTask())

            $"{model}"
        }

In ProgramComponent

It is already injected in ProgramComponent, so you can use it directly without injecting it beforehand.

open Microsoft.JSInterop

type MyApp() =
    inherit ProgramComponent<Model, Message>()

    override this.Program =
        Program.mkSimple init update view
        |> Program.withTrace (fun msg model ->
            this.JSRuntime.InvokeVoidAsync("console.log", msg, model) |> ignore)

In Elmish update commands

It is common to need JavaScript interoperation in the update function to call external functionality. The IJSRuntime can be passed to it from the ProgramComponent. Inside update, the commands located in the module Cmd.OfJS do a JavaScript call and transform its return value into a message. Just like standard Elmish commands, Cmd.OfJS.either also transforms potential exceptions into a message, whereas Cmd.OfJS.perform ignores such exceptions.

open Microsoft.JSInterop

type Message =
    | CallMyJSFunc of MyJSFuncArgType
    | CalledMyJSFunc of MyJSFuncReturnType
    | Error of exn

let update (js: IJSRuntime) message model =
    match message with
    | CallMyJSFunc data ->
        let cmd = Cmd.OfJS.either js "MyJsLib.myJSFunc" [| data |] CalledMyJSFunc Error
        model, cmd
    // ...

type MyApp() =
    inherit ProgramComponent<Model, Message>()
    
    override this.Program =
        let update = update this.JSRuntime
        Program.mkProgram init update view

These functions really are simple wrappers around Cmd.OfTask.either/perform. For example, the above call is equivalent to:

let cmd =
    Cmd.OfTask.either
        (fun args -> js.InvokeAsync("MyJsLib.myJSFunc", args).AsTask())
        [| data |] CalledMyJSFunc Error

HTML element references

Blazor's type ElementReference allows passing a reference to a rendered HTML element to JavaScript. This is useful for interacting with JavaScript libraries that insert themselves in a given element, creating for example a map or a rich text editor; or libraries that interact with more fundamental JavaScript APIs, like focusing an element.

In Bolero, the type HtmlRef is a small utility that makes working with ElementReference from F# simple.

  1. The element to bind must be inside a Component class.
  2. Create an HtmlRef as a field of this class.
  3. To bind it to an element, list it in the computation expression after attributes and before child nodes.
  4. To use it, use its .Value property. It has type ElementReference option: its value is Some if the ref is bound in step 3, and None otherwise.

For example, given this small JavaScript function that can focus a DOM element it receives as argument:

const MyJsLib = {
    focus: function(elt) {
        elt.focus();
    }
}

This function can be called as follows from a Bolero component:

type MyInputWithFocusButton() = // (1)
    inherit ElmishComponent<string, string>()

    let inputRef = HtmlRef() // (2)

    [<Inject>]
    member val JSRuntime = Unchecked.defaultof<IJSRuntime> with get, set

    override this.View model dispatch =
        concat {
            input {
                bind.input.string model dispatch
                inputRef // (3)
            }
            button {
                on.task.click (fun _ ->
                    match inputRef.Value with // (4)
                    | Some ref -> this.JSRuntime.InvokeVoidAsync("MyJsLib.focus", ref).AsTask()
                    | None -> Task.CompletedTask
                )

                "Focus this input box"
            }
        }

Blazor component references

Just like with HTML elements, it is possible to capture a reference to an instantiated Blazor component. Whereas HTML element references are mostly useful with JavaScript interop, Blazor component references are used directly in F# to eg. call methods on the component itself.

Capturing a Blazor component reference is done exactly the same way as capturing an HTML element reference, except that the reference type is Ref<Component> instead of HtmlRef, where Component is the component type. For example, given the following component:

type MyComponent() =
    inherit Component()
    
    override this.Render() =
        div { "This is my component" }
        
    member this.Refresh() =
        Console.WriteLine("Refreshing this component!")

you can get a reference to an instance of it as follows:

type MyApplication() =
    inherit Component()
    
    let myComponentRef = Ref<MyComponent>()
    
    override this.Render() =
        div {
            comp<MyComponent> { myComponentRef }
            button {
                on.click (fun _ ->
                    match myComponentRef.Value with
                    | Some myComponent -> myComponent.Refresh()
                    | None -> ())

                "Refresh my component!"
            }
        }

CSS isolation

Blazor provides a mechanism called CSS isolation that ties a CSS style sheet with a given component type.

Using components with CSS isolation

To use the isolated styles from component libraries you reference, make sure that your HTML content includes a reference to "ASSEMBLYNAME.styles.css", where ASSEMBLYNAME is the name of your project.

This reference is automatically included in all variants of the bolero-app project templates.

Adding CSS isolation to your component

Here are the steps to create an isolated style sheet for your own component:

  1. Create a file with extension .bolero.css. For example, MyStyleSheet.bolero.css:

    a.active {
        background: lightblue;
    }
    
  2. Compile the project once. This will generate a source file containing a module CssScopes.

  3. Add the generated scope to your component:

    type MyComponent() =
        inherit Component()
    
        override _.CssScope = CssScopes.MyStyleSheet
    
        override _.Render() =
            a {
                attr.href "https://fsbolero.io"
                attr.``class`` "active"
                "Go to Bolero!"
            }
        ```
    
    

And that's it, the link in MyComponent will appear with a light blue background!

The property CssScope is available on all Bolero component base types, including Component, ElmishComponent<'model, 'msg> and ProgramComponent<'model, 'msg>.

The name of the stylesheet in the module CssScopes is based on the file name; it can be customized in Client.fsproj:

<ItemGroup>
  <BoleroScopedCss Update="MyStylesheet.bolero.css" ScopeName="MyScope" />
</ItemGroup>