A SAFE-style template with Fable.Lit, Fable.Remoting and Giraffe
Based on: https://github.com/Zaid-Ajaj/SAFE.Simplified (thank you Zaid!)
- RPC to WebApi via Fable.Remoting
- Page routing via Fable.LitRouter
- Shared context via Fable.LitStore
Shoelace
andFluentUI
web components imported (cherry-picked)- A minimal
vite.config.js
file that configures https proxy server + a common proxy redirects "vite-plugin-mkcert
plugin installed for https support for proxy server- Bootstrap icons + a
bs-icon
custom element control. - Toast notifications
- Form Validation (rules lives with entities in Shared.fs)
- Giraffe
- Fable.Remoting + custom error handler
- A very simple REST module
- Environment specific settings files already configured
- Serilog logger
- Entity Validation (rules live with entities in Shared.fs)
dotnet new install fable-lit-fullstack-template
This will create a new subfolder, MyLitApp
, which will contain a MyLitApp.sln
:
dotnet new flft -o MyLitApp
To do the initial restore of both the WebApi and WebLit projects:
- 📂 Build:
dotnet run Restore
Or you can manually restore each:
- 📂 WebApi:
dotnet restore
- 📂 WebLit:
npm install
- 📂 WebApi:
dotnet watch
- 📂 WebLit:
npm start
To build WebApi and WebLit in Release mode and output to the Template/dist
folder:
- 📂 Build:
dotnet run Pack
or - 📂 Build:
dotnet run PackNoTests
Be sure to install the appropriate IDE extension for html and css syntax coloring within your html $""" """
templates!
If using VS Code:
If using Visual Studio:
Currently, VS Code with the "Highlight HTML/SQL Templates in F#" extension provides the best experience because it actually provides contextual IntelliSense for the HTML and CSS, plus you can use all the other amazing HTML extensions.
You can create toast messages in two ways:
- Call a
Toast
function directly:
Toast.success $"Name changed to {username}."
- Return a
Toast
Cmd
(if using Elmish):
let update (msg: Msg) (model: Model) =
match msg with
| Save ->
model, Cmd.OfAsync.either Server.api.SaveProjectFiles model.Files SaveCompleted OnError
| SaveCompleted _ ->
model, Toast.Cmd.success "Files saved."
| OnError ex ->
model, Toast.Cmd.error ex.Message
The Validation.fs
module lives in the Shared.fs project and contains functions for creating validation rules.
Usage:
- Create a custom validation method (or function) alongside your entity in the Shared.fs project:
type CatInfo =
{
Name: string
Age: int
LastVetCheckup: System.DateTime
}
member this.Validate() =
rules
|> rulesFor (nameof this.Name) [
this.Name |> Rules.required
this.Name |> Rules.maxLen 10
]
|> rulesFor (nameof this.Age) [
Rules.isTrue (this.Age > 0) "Age must be a positive number."
]
|> rulesFor (nameof this.LastVetCheckup) [
// A custom rule
let timeSinceLastVetCheckup = System.DateTime.Today - this.LastVetCheckup.Date
printfn $"Total days since last checkup: {timeSinceLastVetCheckup.TotalDays}"
if this.Age >= 10 && timeSinceLastVetCheckup.TotalDays > 90 then
Error "Cats over 10 years old should get a vet checkup every three months."
elif timeSinceLastVetCheckup.TotalDays > 180 then
Error "Cats under 10 years old should get a vet checkup every six months."
else
Ok ()
]
|> validate
- In your WebLit.fs UI / form, track the entity state in your model using the
ValidationResult
:
type Model =
{
Cat: CatInfo
Validation: ValidationResult
Saved: bool
}
let init () =
{
Cat =
{ CatInfo.Name = ""
; CatInfo.Age = 0
; CatInfo.LastVetCheckup = System.DateTime.MinValue }
Validation = noErrors
Saved = false
}, Cmd.none
- In the Elmish
update
function, update theValidation
state by calling the customValidate
method when saving:
let update msg model =
match msg with
| Save ->
let validation = model.Cat.Validate()
{ model with
Validation = validation
Saved = validation.HasErrors() = false
}, Toast.Cmd.success "Changes saved."
- In the form, set the
invalid
attributes of your inputs by checking themodel.Validation
state property for the given property:
<sl-input
label="Cat Name"
.value={model.Cat.Name}
.invalid={model.Validation.HasErrors(nameof model.Cat.Name)}
@sl-change={Ev (fun e -> SetCat { model.Cat with Name = e.target.Value } |> dispatch)}>
</sl-input>
<sl-input
label="Age"
type="number"
.invalid={model.Validation.HasErrors(nameof model.Cat.Age)}
.value={model.Cat.Age}
@sl-change={Ev (fun e -> SetCat { model.Cat with Age = e.target?valueAsNumber } |> dispatch)}>
</sl-input>
<sl-input
label="Last Vet Checkup"
type="date"
.invalid={model.Validation.HasErrors(nameof model.Cat.LastVetCheckup)}
.value={model.Cat.LastVetCheckup.ToString("yyyy-MM-dd")}
@sl-change={Ev (fun e ->
let date = System.DateTime.Parse(e.target.Value)
SetCat { model.Cat with LastVetCheckup = date } |> dispatch
)}>
</sl-input>
- At the top of the form, display the validation errors using the
Ctrls.ValidationSummary
:
<div>
{ValidationSummary(model.Validation)}
</div>
- The validation rules may also be reused on the server side:
let saveCatInfo(catInfo: CatInfo) =
match catInfo.Validate().IsValid() with
| true -> // save
| false -> // reject