A SAFE-style template with Fable.Lit, Fable.Remoting and Giraffe
Based on: https://github.com/Zaid-Ajaj/SAFE.Simplified (thank you Zaid!)
Shoelace
and FluentUI
web components imported (cherry-picked)vite.config.js
file that configures https proxy server + a common proxy redirects"vite-plugin-mkcert
plugin installed for https support for proxy serverbs-icon
custom element control.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:
dotnet run Restore
Or you can manually restore each:
dotnet restore
npm install
dotnet watch
npm start
To build WebApi and WebLit in Release mode and output to the Template/dist
folder:
dotnet run Pack
ordotnet 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:
1) Call a Toast
function directly:
Toast.success $"Name changed to {username}."
2) 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:
1) 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
2) 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
3) In the Elmish update
function, update the Validation
state by calling the custom Validate
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."
4) In the form, set the invalid
attributes of your inputs by checking the model.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>
5) At the top of the form, display the validation errors using the Ctrls.ValidationSummary
:
<div>
{ValidationSummary(model.Validation)}
</div>
6) The validation rules may also be reused on the server side:
let saveCatInfo(catInfo: CatInfo) =
match catInfo.Validate().IsValid() with
| true -> // save
| false -> // reject