some module organization

This commit is contained in:
caleb 2025-07-27 22:58:04 -07:00
parent d50eb26b14
commit c03182e08c
12 changed files with 298 additions and 40 deletions

View File

@ -1,23 +0,0 @@
namespace web_api_cookbook
open WebSharper
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
open Units.Animation
open Units.Time
[<JavaScript>]
module Client =
[<SPAEntryPoint>]
let Main () =
let isClicked = Var.Create false
let opacityAnimated = Animate.valueWhen isClicked.View 1.0 0. 120.<frames/s> 1.<s>
let opacityStyle = View.MapCached (sprintf "opacity: %f") opacityAnimated
let onClick = on.click (fun _ _ -> Var.Set isClicked true)
div [] [
UI.Components.button [attr.styleDyn opacityStyle; onClick] "Hide Me"
]
|> Doc.RunById "main"

58
src/Client.fs Normal file
View File

@ -0,0 +1,58 @@
namespace web_api_cookbook
open web_api_cookbook
open web_api_cookbook.Exercises
open WebSharper
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
[<JavaScript>]
module Exercise =
let private c = attr.``class``
let private showHide () =
let show = Var.Create false
let text =
function
| true -> "(Hide)"
| false -> "(Show)"
|> View.MapCached <| show.View
let toggle =
fun _ _ curr ->
Var.Set show (not curr)
|> on.clickView show.View
show.View, div [c "button button--text"; toggle] [textView text]
let doc title description content =
let expanded, showHide = showHide ()
div [c "exercise"] [
h2 [c "exercise__title"] [showHide; text title]
fun () ->
div [c "exercise__section"] [
div [c "exercise__description"] [text description]
div [c "exercise__content"] [content ()]
]
|> Doc.When expanded
]
[<JavaScript>]
module Client =
[<SPAEntryPoint>]
let Main () =
div [] [
h1 [] [text "Using Various Browser Web APIs via WebSharper (in F#)"]
p [] [
text "WebSharper is a web framework that provides a functional reactive programming '-ish' API for use in web development. It supports a variety of applicative and monadic combinators that work natuarally with F#, making it a pleasure to use."]
p [] [text "On this page I've implemented code snippets that use various features of the browser's Web API, and WebSharper. The source code can be viewed "; a [] [text "here."]]
Exercise.doc
"Using requestAnimationFrame"
"The requestAnimationFrame function is used to provide a callback to the browser, which is invoked before the browser repaints the page. This can be used to efficiently do animations asynchronously. This snippet adjusts the opacity of a button over time after it is clicked."
RequestAnimationFrame.doc
Exercise.doc
"Syncing Data Across Tabs via LocalStorage"
"Besides simply storing data, LocalStorage can be used as a simple method for syncing state between different tabs. In addition to storing the data, storage events must also be observed to keep the data synchronized. You can see this in action by opening this page in two tabs and entering/deleting data using the below form."
LocalStorageSync.doc
]
|> Doc.RunById "main"

View File

@ -0,0 +1,57 @@
namespace web_api_cookbook.Exercises
open web_api_cookbook
open web_api_cookbook.UI
open web_api_cookbook.UI.Components
open WebSharper
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
[<JavaScript>]
module LocalStorageSync =
let doc () =
let resetKeyInput, keyInput, keyInputDoc = InputType.text [] "Key: "
let resetValInput, valInput, valInputDoc = InputType.text [] "Value: "
let args = View.Map2 tuple2 keyInput valInput
let clear () =
resetKeyInput ()
resetValInput ()
let onAdd =
fun _ _ (key, value) ->
LocalStorage.setItem key value
clear ()
|> on.clickView args
let addButton = Button.plain [onAdd] "Add Element"
let addedElements =
fun k element ->
let value = View.Map snd element
let onDelete =
fun _ _ ->
LocalStorage.removeItem k
|> on.click
Doc.Concat [
div [] [text <| sprintf "Key: %s" k]
div [] [textView <| View.Map (sprintf "Value: %s") value]
Button.plain [onDelete] "Delete Element"
]
|> Doc.BindSeqCachedViewBy fst <| LocalStorage.view ()
Doc.Concat [
div [] [text "Elements Added to Local Storage:"]
div [attr.``class`` "gridwrap"] [
addedElements
]
div [] [text "Add New Element:"]
div [attr.``class`` "gridwrap"] [
keyInputDoc
valInputDoc
addButton
]
]

View File

@ -0,0 +1,23 @@
namespace web_api_cookbook.Exercises
open WebSharper
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
open web_api_cookbook.UI
open web_api_cookbook.Units.Animation
open web_api_cookbook.Units.Time
open web_api_cookbook.UI.Components
[<JavaScript>]
module RequestAnimationFrame =
let doc () =
let isClicked = Var.Create false
let opacityAnimated = Animate.valueWhen isClicked.View 1.0 0. 120.<frames/s> 1.<s>
let opacityStyle = View.MapCached (sprintf "opacity: %f") opacityAnimated
let animateOnClick =
fun _ _ ->
Var.Set isClicked true
|> on.click
Button.plain [attr.styleDyn opacityStyle; animateOnClick] "Hide Me"

View File

@ -1,6 +1,8 @@
namespace web_api_cookbook
open WebSharper
open WebSharper.UI
open WebSharper.UI.Client
[<JavaScript>]
module Option =
@ -20,6 +22,15 @@ module Units =
[<Measure>]
type s
[<JavaScript>]
module Doc =
let When show content =
function
| true -> content ()
| false -> Doc.Empty
|> View.MapCached <| show
|> Doc.EmbedView
[<JavaScript>]
module Math =
let clamp a b c =
@ -29,3 +40,8 @@ module Math =
[<Inline>]
let inline lerp a b t = a + t * (b - a)
[<AutoOpen>]
[<JavaScript>]
module Tuple2 =
let tuple2 a b = a,b

View File

@ -1,21 +1,15 @@
namespace web_api_cookbook
namespace web_api_cookbook.UI
open web_api_cookbook
open web_api_cookbook.Units.Animation
open web_api_cookbook.Units.Time
open WebSharper
open WebSharper.JavaScript
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
[<JavaScript>]
module UI =
module Components =
let button attrs label =
button (attrs @ [attr.``type`` "button"]) [ text label ]
open WebSharper.JavaScript
[<JavaScript>]
module Animate =
open Units.Animation
open Units.Time
let value (startPoint:float) endPoint (targetFps:float<frames/s>) (animationSeconds:float<s>) =
let frameInterval = 1. / targetFps * 1000.0<ms/s>
let frameCount = animationSeconds * targetFps

23
src/UI/Components.fs Normal file
View File

@ -0,0 +1,23 @@
namespace web_api_cookbook.UI
open WebSharper
open WebSharper.JavaScript
open WebSharper.UI
open WebSharper.UI.Client
open WebSharper.UI.Html
[<JavaScript>]
module Components =
module Button =
let plain attrs label =
button (attrs @ [attr.``type`` "button"; attr.``class`` "button"]) [ text label ]
module InputType =
let reset var () =
Var.Set var ""
let text attrs label =
let inputVal = Var.Create ""
let input = Doc.InputType.Text attrs inputVal
let doc = span [] [text label; input]
reset inputVal, inputVal.View, doc

54
src/UI/LocalStorage.fs Normal file
View File

@ -0,0 +1,54 @@
namespace web_api_cookbook.UI
open WebSharper
open WebSharper.UI
open WebSharper.JavaScript
[<JavaScript>]
module LocalStorage =
let allItems : Var<list<string * string>> = Var.Create []
let getItem (key: string) : option<string> =
match JS.Window.LocalStorage.GetItem key with
| null -> None
| value -> Some value
let private putItem (key: string) (value:string) : unit =
Var.Get allItems
|> List.filter (fun (k, _) -> k <> key)
|> fun filtered -> (key, value) :: filtered
|> Var.Set allItems
let private deleteItem (key: string) : unit =
Var.Get allItems
|> List.filter (fun (k, _) -> k <> key)
|> Var.Set allItems
let setItem (key: string) (value: string) : unit =
JS.Window.LocalStorage.SetItem(key, value)
putItem key value
let removeItem (key: string) : unit =
JS.Window.LocalStorage.RemoveItem(key)
deleteItem key
JS.Window.AddEventListener("storage", fun (e:Dom.Event) ->
let key:string option = e?key |> Option.ofObj
let newValue:string option = e?newValue |> Option.ofObj
Console.Info key
Console.Info newValue
match key, newValue with
| Some key, Some newVal -> putItem key newVal
| Some key, None -> removeItem key
| _ -> ())
let view () =
let items =
[
for i in 0 .. JS.Window.LocalStorage.Length - 1 do
let key = JS.Window.LocalStorage.Key i
let value = JS.Window.LocalStorage.GetItem key
yield (key, value)
]
allItems.Set items
allItems.View

View File

@ -9,10 +9,14 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Prelude.fs" />
<Compile Include="Ui.fs" />
<Compile Include="Client.fs" />
<Compile Include="Startup.fs" />
<Compile Include="src/Prelude.fs" />
<Compile Include="src/UI/LocalStorage.fs" />
<Compile Include="src/UI/Components.fs" />
<Compile Include="src/UI/Animate.fs" />
<Compile Include="src/Exercises/RequestAnimationFrame.fs" />
<Compile Include="src/Exercises/LocalStorageSync.fs" />
<Compile Include="src/Client.fs" />
<Compile Include="src/Startup.fs" />
<None Include="package.json" />
<None Include="esbuild.config.mjs" />
<None Include="vite.config.js" />

View File

@ -4,7 +4,7 @@
<title>web_api_cookbook</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="Scripts/web_api_cookbook.css" />
<link rel="stylesheet" type="text/css" href="style.css" />
<script type="text/javascript" src="Scripts/web_api_cookbook.head.js"></script>
</head>
<body>

52
wwwroot/style.css Normal file
View File

@ -0,0 +1,52 @@
h1 {
font-size: 1.2rem; /* or 28px */
margin-bottom: 0.5em;
}
h2 {
font-size: 1.1rem; /* or 22px */
margin-bottom: 0.4em;
}
h1, h2 {
line-height: 1.2;
}
.gridwrap {
display: grid;
grid-template-columns: repeat(3, 15em);
}
.exercise {
margin_bottom: .5em;
}
.exercise__title {
display: flex;
gap: .5em;
align-items: center;
margin-bottom: .5em;
}
.exercise__section {
border-left: 2px solid lightgray;
padding-left: .5em;
}
.exercise__description {
color: #666;
font-style: italic;
}
.exercise__content {
margin-top: .5em;
}
.button {
cursor: pointer;
}
.button--text {
font-style:italic;
color: blue
}