From 7de147dabb19b87e629445992976979553a4db26 Mon Sep 17 00:00:00 2001 From: caleb Date: Sun, 27 Jul 2025 22:58:04 -0700 Subject: [PATCH] some module organization --- Client.fs | 52 ++++++++++++++++++++++----- Exercises/LocalStorageSync.fs | 57 ++++++++++++++++++++++++++++++ Exercises/RequestAnimationFrame.fs | 23 ++++++++++++ Prelude.fs | 16 +++++++++ Ui.fs => UI/Animate.fs | 18 ++++------ UI/Components.fs | 23 ++++++++++++ UI/LocalStorage.fs | 54 ++++++++++++++++++++++++++++ web_api_cookbook.fsproj | 6 +++- wwwroot/index.html | 2 +- wwwroot/style.css | 52 +++++++++++++++++++++++++++ 10 files changed, 281 insertions(+), 22 deletions(-) create mode 100644 Exercises/LocalStorageSync.fs create mode 100644 Exercises/RequestAnimationFrame.fs rename Ui.fs => UI/Animate.fs (85%) create mode 100644 UI/Components.fs create mode 100644 UI/LocalStorage.fs create mode 100644 wwwroot/style.css diff --git a/Client.fs b/Client.fs index a84ab0b..2ec4ad1 100644 --- a/Client.fs +++ b/Client.fs @@ -1,23 +1,59 @@ 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 -open Units.Animation -open Units.Time +[] +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 + ] [] module Client = [] let Main () = - let isClicked = Var.Create false - let opacityAnimated = Animate.valueWhen isClicked.View 1.0 0. 120. 1. - 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" + 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." + text "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 using the browser's Web API, using WebSharper. The source code can be viewed "; a [] [text "here."]] + Exercise.doc + "Using requestAnimationFrame" + "Trying to use Request Animation Frame" + RequestAnimationFrame.doc + Exercise.doc + "Syncing LocalStorage Across Tabs" + "Using LocalStorage to keep data synced between tabs." + LocalStorageSync.doc ] |> Doc.RunById "main" diff --git a/Exercises/LocalStorageSync.fs b/Exercises/LocalStorageSync.fs new file mode 100644 index 0000000..2dd0a53 --- /dev/null +++ b/Exercises/LocalStorageSync.fs @@ -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 + +[] +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 + ] + ] diff --git a/Exercises/RequestAnimationFrame.fs b/Exercises/RequestAnimationFrame.fs new file mode 100644 index 0000000..83f9915 --- /dev/null +++ b/Exercises/RequestAnimationFrame.fs @@ -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 + +[] +module RequestAnimationFrame = + let doc () = + let isClicked = Var.Create false + let opacityAnimated = Animate.valueWhen isClicked.View 1.0 0. 120. 1. + 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" diff --git a/Prelude.fs b/Prelude.fs index e6b8629..d881227 100644 --- a/Prelude.fs +++ b/Prelude.fs @@ -1,6 +1,8 @@ namespace web_api_cookbook open WebSharper +open WebSharper.UI +open WebSharper.UI.Client [] module Option = @@ -20,6 +22,15 @@ module Units = [] type s +[] +module Doc = + let When show content = + function + | true -> content () + | false -> Doc.Empty + |> View.MapCached <| show + |> Doc.EmbedView + [] module Math = let clamp a b c = @@ -29,3 +40,8 @@ module Math = [] let inline lerp a b t = a + t * (b - a) + +[] +[] +module Tuple2 = + let tuple2 a b = a,b diff --git a/Ui.fs b/UI/Animate.fs similarity index 85% rename from Ui.fs rename to UI/Animate.fs index d2d5f90..0b66f9f 100644 --- a/Ui.fs +++ b/UI/Animate.fs @@ -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 - -[] -module UI = - module Components = - let button attrs label = - button (attrs @ [attr.``type`` "button"]) [ text label ] +open WebSharper.JavaScript [] module Animate = - open Units.Animation - open Units.Time + let value (startPoint:float) endPoint (targetFps:float) (animationSeconds:float) = let frameInterval = 1. / targetFps * 1000.0 let frameCount = animationSeconds * targetFps diff --git a/UI/Components.fs b/UI/Components.fs new file mode 100644 index 0000000..062b822 --- /dev/null +++ b/UI/Components.fs @@ -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 + +[] +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 diff --git a/UI/LocalStorage.fs b/UI/LocalStorage.fs new file mode 100644 index 0000000..fda4262 --- /dev/null +++ b/UI/LocalStorage.fs @@ -0,0 +1,54 @@ +namespace web_api_cookbook.UI + +open WebSharper +open WebSharper.UI +open WebSharper.JavaScript + +[] +module LocalStorage = + let allItems : Var> = Var.Create [] + + let getItem (key: string) : option = + 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 diff --git a/web_api_cookbook.fsproj b/web_api_cookbook.fsproj index fc7d155..bbc7dc9 100644 --- a/web_api_cookbook.fsproj +++ b/web_api_cookbook.fsproj @@ -10,7 +10,11 @@ - + + + + + diff --git a/wwwroot/index.html b/wwwroot/index.html index 947fd9e..997bb9a 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -4,7 +4,7 @@ web_api_cookbook - + diff --git a/wwwroot/style.css b/wwwroot/style.css new file mode 100644 index 0000000..ac8ec3d --- /dev/null +++ b/wwwroot/style.css @@ -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 +}