Repository for wilsoncg.net.
Uses Bolero - F# Tools for Blazor, see Bolero website and repository.
with .NET Core SDK 3.1.300 or newer
dotnet build -c Debug; dotnet run -p .\src\BoleroGitHub.Server\ -c Debug
BoleroGitHub.Server
project uses dotnet Kestrel web server to assist development (template hot reloading). The static site served is contained in BoleroGitHub.Client
project.
To run as staging environment:
dotnet publish -c Release;dotnet run -p .\src\BoleroGitHub.Server\ -c Release --launch-profile "KestrelStaging"
JAMstack is a cloud-native web development architecture based on client-side JavaScript code, BAMstack is a step forward using WebAssembly.
“A modern web development architecture based on client-side Bolero, web Assembly, and prebuilt Markup”
- Cross platform development & delivery with .NET Core & WebAssembly with Bolero
- Elmish Model-View-Update functional approach to building a reactive UI
- Access the full suite of community created Blazor resources
- Utilise MarkDig tool which converts markdown to html
There were two motivating factors which lead to the decision to convert this blog from jekyll to .Net & WebAssembly:
- Randomizing the header splash image on page reload. After some investigation it's a surpisingly non-trivial task. This involes fiddling with liquid syntax & loops in order to fetch the list of image files in a folder, creating a customized page template and finally combining some handcrafted javascript to instruct the browser to randomly fetch an image file.
- Bypassing the jekyll engine for specified sub directories in order to serve up some WebAssembly.
Say goodbye to maintenance of 300MB node_modules directories & potential javascript security issues.
let splashImages = [ "splash-1.jpg"; "splash-2.png"; "splash-3.jpg" ]
let r = Random().Next(splashImages.Length)
Main
.Home()
.SplashImage(splashImages.[r])
.Elt()
type Toggle = On | Off
type Model = { searchToggle: Toggle }
type Message =
| SearchToggle of Toggle
let initModel = { searchToggle = Off }
let update message model =
match message with
| SearchToggle toggle ->
{ model with searchToggle = if toggle = On then Off else On }, Cmd.none
type Main = Template<"wwwroot/templateMainMinimal.html">
let view model dispatch =
Main()
.SearchToggle(fun _ -> dispatch (SearchToggle model.searchToggle))
.ContentIsVisible(if model.searchToggle = On then "is--hidden" else "")
.SearchIsVisible(if model.searchToggle = On then "is--visible" else "")
.Elt()
And some definitions in the html template:
<nav id="site-nav">
<button class="search__toggle" type="button" onClick="${SearchToggle}">
<span>Toggle search</span>
</button>
</nav>
<div class="initial-content ${ContentIsVisible}">
...
</div>
<div class="search-content ${SearchIsVisible}">
...
</div>
First we have to include highlight.js, along with a function for enabling the highlight:
<head>
<script src="/js/highlight-10-1-2.pack.js"></script>
<script type="text/javascript">
window.syntaxHighlight = () =>
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
</script>
</head>
We need a mechanism to trigger the highlighting, as the usual DOM loaded event is not triggered. Fortunately Bolero has an ElmishComponent we can utilize. With the injected IJSRuntime, we can have Blazor asynchronously call the syntaxHighlight() javascript function after it has rendered the component.
type PostBodyComponentModel = { RawHtml: string }
type PostBodyComponent() =
inherit ElmishComponent<PostBodyComponentModel, PostPageMsg>()
[<Inject>]
member val JSRuntime = Unchecked.defaultof<IJSRuntime> with get, set
override this.ShouldRender() = true
override this.View model dispatch =
RawHtml model.RawHtml
override this.OnAfterRenderAsync firstRender =
match firstRender with
| true ->
this.JSRuntime.InvokeVoidAsync("syntaxHighlight").AsTask()
| false -> ValueTask().AsTask()
We then use the Bolero html helper ecomp, which creates an html fragment from our Blazor component. Then the view funcion places that component into the Body hole defined in the html template.
let showPost post title dispatch =
let rendered = post
let postBody =
ecomp<PostBodyComponent,_,_> [] { RawHtml = rendered.Body } dispatch
Main
.Body(postBody)
.Elt()
First we define some Javascript, notice the callback will be provided to the JS environment. We see that the .NET framework will create the machinery for us.
window.generalFunctions = {
env: {
hamburgerVisible: false
},
getSize: function(){
var size = { "height": window.innerHeight, "width" : window.innerWidth };
return size;
},
initResizeCallback: function(onResize) {
window.addEventListener('resize', (ev) => {
this.resizeCallbackJS(onResize);
});
},
resizeCallbackJS: function(callback) {
var size = this.getSize();
if(size.width < 450 && !this.env.hamburgerVisible)
{
this.env.hamburgerVisible = true;
callback.invokeMethodAsync('Invoke', size.height, size.width);
}
if(size.width > 450 && this.env.hamburgerVisible)
{
this.env.hamburgerVisible = false;
callback.invokeMethodAsync('Invoke', size.height, size.width);
}
}
};
This should be loaded after the blazor WASM framework initialization.
<script src="_framework/blazor.webassembly.js"></script>
<script src="/js/windowResize.js"></script>
We then use DotNetObjectReference.Create()
to a DotNet JS interop object which is passed into the Javascript defined above. We can define a helper Callback
type which will be decorated with JSInvokable
, this allows the blazor framework to correctly identify & call the instance method. We create a subscription message during initialization, where the Javascript is instructed to call the Invoke()
method on the .NET object. With this mechanism we have achieved JS interop, where a WindowResize message will be dispatched within Bolero on each window.resize
DOM event.
type Size(h:int, w:int) =
member this.Height with get() = h
member this.Width with get() = w
new() = Size(0,0)
type Callback =
static member OfSize(f) =
DotNetObjectReference.Create(SizeCallback(f))
and SizeCallback(f: Size -> unit) =
[<JSInvokable>]
member this.Invoke(arg1, arg2) =
f (Size(arg1, arg2))
type Message =
| Initialize
| WindowResize of Size
let update (jsRuntime:IJSRuntime) message model =
let setupJSCallback =
Cmd.ofSub (fun dispatch ->
// given a size, dispatch a message
let onResize = dispatch << WindowResize
jsRuntime.InvokeVoidAsync("generalFunctions.initResizeCallback", Callback.OfSize onResize).AsTask() |> ignore
)
match message with
| Initialize -> model, setupJSCallback
| WindowResize size ->
// handle window resize message
model, Cmd.none
As WebAssembly runs inside the browser sandbox, if we attempt to load a markdown file using System.File.IO.ReadLAllText()
we will receive an error from the mono wasm runtime. To overcome this we first need to hook into msbuild with a custom build target which generates an index of markdown files.
<Target Name="GenerateIndexJsonForPostsFolder">
<ItemGroup>
<_MarkdownPosts
Include="$(_BlazorCurrentProjectWWWroot)\posts\**\*.md" />
<_MarkdownPostsRelative
Include="@(_MarkdownPosts->'posts/%(Filename)%(Extension)')" />
</ItemGroup>
<WriteLinesToFile
File="$(_BlazorCurrentProjectWWWroot)\posts\index.txt"
Lines="@(_MarkdownPostsRelative)"
Overwrite="true"
Encoding="Unicode"/>
</Target>
<PropertyGroup>
<_BlazorCopyFilesToOutputDirectoryDependsOn>
$(_BlazorCopyFilesToOutputDirectoryDependsOn);
GenerateIndexJsonForPostsFolder
</_BlazorCopyFilesToOutputDirectoryDependsOn>
</PropertyGroup>
We can then asynchronously fetch the /posts/index.txt
file from the server, then for each markdown file listed in index.txt
, we can retrieve and parse the markdown. You could argue that this is redundant as Bolero already has an inbuilt html templating mechanism, but I wanted to see if it's possible to keep the existing jekyll markdown files and combine the two rendering systems. The full implementation details can be found in PostPage.fs
& Markdown.fs
.
- FBlazorShop - An F# implementation of Steve Sanderson's pizza store blazor app workshop
- TryFSharpOnWasm - F# compiler running in WebAssembly with Bolero. A useful working reference application showing JS interop & other Bolero/Blazor features.