Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add facilities for StreamRendering #356

Merged
merged 3 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Bolero.Server/Components.fs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ type BoleroScript() =
override this.BuildRenderTree(builder) =
builder.AddMarkupContent(0, BoleroHostConfig.Body(this.Config))

#if NET8_0_OR_GREATER
[<AbstractClass; StreamRendering true>]
type StreamRenderingComponent<'model>() =
inherit Component<'model>()

let mutable model = Unchecked.defaultof<'model>

abstract InitialModel : 'model

abstract LoadModel : 'model -> Task<'model>

abstract Render : 'model -> Node

override this.OnInitializedAsync() =
model <- this.InitialModel
task {
let! newModel = this.LoadModel(model)
model <- newModel
}

override this.Render() =
this.Render(model)
#endif

module Rendering =

let private emptyContent = Task.FromResult { new IHtmlContent with member _.WriteTo(_, _) = () }
Expand Down
18 changes: 15 additions & 3 deletions src/Bolero/Components.fs
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ and [<AbstractClass>]
with _ -> () // fails if run in prerender
)

override this.OnInitialized() =
base.OnInitialized()
override this.OnInitializedAsync() =
let setDispatch d =
dispatch <- d
program <-
Expand All @@ -207,12 +206,25 @@ and [<AbstractClass>]
id id
(fun _ model dispatch -> setState model dispatch)
id id
runProgramLoop <- Program'.runFirstRender this program

let updateInitState, initModel, loop = Program'.runFirstRender this program
runProgramLoop <- loop
setState <- fun model dispatch ->
match oldModel with
| Some oldModel when this.ShouldRender(oldModel, model) -> this.ForceSetState(model, dispatch)
| _ -> ()

match this.StreamingInit with
| None ->
Task.CompletedTask
| Some init ->
task {
let! model, cmd = init initModel
updateInitState model cmd
}

member val internal StreamingInit : ('model -> Task<'model * Cmd<'msg>>) option = None with get, set

member internal this.InitRouter
(
r: IRouter<'model, 'msg>,
Expand Down
40 changes: 40 additions & 0 deletions src/Bolero/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,48 @@
module Bolero.Program

open System.Reflection
open System.Threading.Tasks
open Elmish

/// <summary>
/// Create a simple program for a component that uses StreamRendering.
/// </summary>
/// <param name="initialModel">The model that is shown initially.</param>
/// <param name="load">Load the model to be stream-rendered.</param>
/// <param name="update">The Elmish update function.</param>
/// <param name="view">The Elmish view function.</param>
let mkSimpleStreamRendering
(initialModel: 'model)
(load: 'model -> Task<'model>)
(update: 'msg -> 'model -> 'model)
(view: 'model -> Dispatch<'msg> -> Node)
: Program<'model, 'msg> =
Program.mkSimple (fun (comp: ProgramComponent<'model, 'msg>) ->
comp.StreamingInit <- Some (fun x -> task {
let! model = load x
return model, Cmd.none
})
initialModel)
update view

/// <summary>
/// Create a program for a component that uses StreamRendering.
/// </summary>
/// <param name="initialModel">The model that is shown initially.</param>
/// <param name="load">Load the model to be stream-rendered.</param>
/// <param name="update">The Elmish update function.</param>
/// <param name="view">The Elmish view function.</param>
let mkStreamRendering
(initialModel: 'model)
(load: 'model -> Task<'model * Cmd<'msg>>)
(update: 'msg -> 'model -> 'model * Cmd<'msg>)
(view: 'model -> Dispatch<'msg> -> Node)
: Program<'model, 'msg> =
Program.mkProgram (fun (comp: ProgramComponent<'model, 'msg>) ->
comp.StreamingInit <- Some load
initialModel, [])
update view

/// <summary>
/// Attach `router` to `program` when it is run as the `Program` of a `ProgramComponent`.
/// </summary>
Expand Down
11 changes: 10 additions & 1 deletion src/Bolero/ProgramRun.fs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,17 @@ module internal Program' =

reentered <- true
setState model dispatch
fun () ->
let mutable cmd = cmd

let updateInitState m cmd' =
setState m dispatch
state <- m
cmd <- cmd @ cmd'

let run () =
cmd |> Cmd.exec (fun ex -> onError ("Error intitializing:", ex)) dispatch
activeSubs <- Subs.diff activeSubs sub |> Subs.Fx.change onError dispatch
processMsgs ()
reentered <- false

updateInitState, model, run
19 changes: 14 additions & 5 deletions tests/Remoting.Client/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@

module Bolero.Tests.Remoting.Client

open System
open System.Collections.Generic
open System.Threading.Tasks
open Microsoft.AspNetCore.Components
open Microsoft.AspNetCore.Components.Authorization
open Bolero
open Bolero.Html
open Bolero.Remoting
open Bolero.Remoting.Client
open Elmish
open Microsoft.Extensions.Logging

type MyApi =
{
Expand Down Expand Up @@ -220,22 +224,27 @@ let Display model dispatch =
}
}

[<StreamRendering true>] //; BoleroRenderMode(BoleroRenderMode.Server, prerender = false)>]
type MyApp() =
inherit ProgramComponent<Model, Message>()

override this.Program =
let myApi = this.Remote<MyApi>()
Program.mkProgram (fun _ -> InitModel, Cmd.batch [
let load model = task {
do! Task.Delay 2000
return { model with currentKey = 42 }, Cmd.batch [
Cmd.ofMsg RefreshItems
Cmd.ofMsg GetLogin
]) (Update myApi) Display
]
}

override this.Program =
let myApi = this.Remote<MyApi>()
Program.mkStreamRendering InitModel load (Update myApi) Display
|> Program.withRouter router


open Microsoft.AspNetCore.Components.WebAssembly.Hosting
open Microsoft.Extensions.DependencyInjection
open System.Security.Claims
open System.Threading.Tasks

type DummyAuthProvider() =
inherit AuthenticationStateProvider()
Expand Down
16 changes: 15 additions & 1 deletion tests/Remoting.Server/Startup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace Bolero.Tests.Remoting

open System
open System.Text.Json.Serialization
open System.Threading.Tasks
open Bolero.Tests.Remoting.Client
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Authentication.Cookies
Expand All @@ -40,14 +41,27 @@ module Page =
open Bolero.Html
open Bolero.Server.Html

type MyStreamedComponent() =
inherit Components.StreamRenderingComponent<string>()

override _.InitialModel = "loading..."

override _.LoadModel(_initialModel) = task {
do! Task.Delay (TimeSpan.FromSeconds 2)
return "loaded!"
}

override _.Render(model) = div { $"Static stream content {model}" }

let index = doctypeHtml {
head {
title { "Bolero (remoting)" }
meta { attr.charset "UTF-8" }
``base`` { attr.href "/" }
}
body {
div { attr.id "main"; comp<MyApp> { attr.renderMode RenderMode.InteractiveAuto } }
div { attr.id "main"; comp<MyApp> { attr.renderMode (InteractiveWebAssemblyRenderMode(prerender = false)) } }
comp<MyStreamedComponent>
script { attr.src "_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js" }
boleroScript
}
Expand Down
Loading