WebAssembly or server-side
Note: Blazor 8 and Bolero 0.24 introduced new hosting models documented here. This page describes the pre-Blazor 8 hosting models, which are still supported but considered legacy.
Bolero applications, like all Blazor applications, can run in a number of different modes.
In plain WebAssembly, all F# code runs in the browser using WebAssembly and the static content is a simple HTML file.
In hosted modes, there is an ASP.NET Core server side that serves the static content, and dynamic behavior can run either in WebAssembly, or on the server side using a SignalR connection to update the browser.
To create a plain WebAssembly application, use the dotnet project template with the following arguments:
dotnet new bolero-app --render=WebAssembly
In plain WebAssembly mode, the static content of the application is a simple .html file, located at src/<Project>.Client/wwwroot/index.html.
The dynamic Bolero content is rendered in a tag of this file.
This file must contain the following:
<script src="_framework/blazor.webassembly.js"></script>
as well as a container element for the dynamic content, for example <div id="main"></div>.
This container is selected in Startup.fs:
builder.RootComponents.Add<Main.MyApp>("#main")
Note that prerendering is not possible in plain WebAssembly mode.
To create a hosted WebAssembly application, use the dotnet project template with the following arguments:
dotnet new bolero-app --render=InteractiveWebAssembly
In InteractiveWebAssembly mode, the static content of the application is served by an ASP.NET Core server.
It is defined in Bolero HTML syntax in the file <Project>.Server/Index.fs.
To create a server-side application, use the dotnet project template with the following arguments:
dotnet new bolero-app --render=InteractiveServer
In InteractiveServer mode, both the static and dynamic content of the application run in an ASP.NET Core server.
The static content is defined in Bolero HTML syntax in the file <Project>.Server/Index.fs.
The dynamic content runs on the server side using a SignalR connection to update the browser.
To create an auto-hosted application, use the dotnet project template with the following arguments:
dotnet new bolero-app --render=InteractiveAuto
In InteractiveAuto mode, the application determines automatically whether it should run in InteractiveServer or InteractiveWebAssembly mode. Essentially, the first time a user connects to the site, it runs in InteractiveServer mode, but also downloads the WebAssembly assets in the background. Then, on subsequent connections, it runs in InteractiveWebAssembly mode using the assets that are already loaded.
Stream rendering is a Blazor feature that allows serving an initial version of a page while launching an asynchronous task that will dynamically update the page with updated content.
It can be used when the page's proper content is slow to load, to display temporary content: in general, either a loader or a cached but potentially outdated version of the content.
Stream rendering is supported in all hosted modes.
To stream render static content, add a component in the server project inheriting from StreamRenderingComponent<'T>, where 'T is the type of the component's data model.
This type has three members to override:
InitialModel: 'T is the data model to display initially.LoadModel : 'T -> Task<'T> loads the data model to stream dynamically. It takes InitialModel as argument.Render : 'T -> Node indicates how to render the data model.For example, loading the data from a database repository:
type Item = { name: string; price: decimal }
type PriceTable() =
inherit StreamRenderingComponent<Item array>()
[<Inject>]
member val IPriceRepository PriceRepository = null with get, set
override _.InitialModel = [| { name = "Loading prices..."; price = 0m } |]
override this.LoadModel(_initialModel) = task {
let! data = this.PriceRepository.GetPricesAsync()
return data
}
override _.Render(model) =
table {
thead {
tr { th { "Name" }; th { "Price" } }
}
tbody {
for item in model do
tr { td { item.name }; td { $"{item.price}" } }
}
}
And then to include the content in the page:
let index = doctypeHtml {
head { (* ... *) }
body {
h1 { "Price table" }
comp<PriceTable>
boleroScript
}
}
For content that will continue to be dynamic after the stream rendering, you can use a stream rendered Elmish program.
Add the attribute [<StreamRendering true>] to your ElmishComponent<'model, 'msg>.
Instead of creating the Elmish program with Program.mkSimple or Program.mkProgram, use Program.mkSimpleStreamRendering or Program.mkStreamRendering, respectively.
mkSimpleStreamRendering, in addition to an initialModel: 'model, also takes a function load: 'model -> Task<'model> which loads the model to stream dynamically.
mkStreamRendering, instead of a function init which returns an initial model and commands, takes:
an initialModel: 'model which is rendered initially;
a function loadModel: 'model -> Task<'model> * Cmd<'msg> which loads the model to stream dynamically as well as initial commands, if any.
The following example loads the model by calling a remote function:
type Remote = { getCounter: unit -> Async<int> }
type Model = { counter: int }
type Msg = Increment | Decrement
let initialModel = { counter = 0 }
let loadModel (remote: Remote) _ = task {
let! counter = remote.getCounter()
return { counter = counter }
}
let update msg model =
match msg with
| Decrement -> { counter = model.counter - 1 }
| Increment -> { counter = model.counter + 1 }
let view model dispatch =
concat {
button { "-"; on.click (fun _ -> dispatch Decrement) }
$" {model.counter} "
button { "+"; on.click (fun _ -> dispatch Increment) }
}
[<StreamRendering true>]
type Counter() =
inherit ElmishComponent<Model, Msg>()
override _.Program
Program.mkSimpleStreamRendering initialModel loadModel update view