diff --git a/Client.fs b/Client.fs deleted file mode 100644 index a84ab0b..0000000 --- a/Client.fs +++ /dev/null @@ -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 - - -[] -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" - ] - |> Doc.RunById "main" diff --git a/src/Client.fs b/src/Client.fs new file mode 100644 index 0000000..ae75c9a --- /dev/null +++ b/src/Client.fs @@ -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 + +[] +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 () = + 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" diff --git a/src/Exercises/LocalStorageSync.fs b/src/Exercises/LocalStorageSync.fs new file mode 100644 index 0000000..2dd0a53 --- /dev/null +++ b/src/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/src/Exercises/RequestAnimationFrame.fs b/src/Exercises/RequestAnimationFrame.fs new file mode 100644 index 0000000..83f9915 --- /dev/null +++ b/src/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/src/Prelude.fs similarity index 63% rename from Prelude.fs rename to src/Prelude.fs index e6b8629..d881227 100644 --- a/Prelude.fs +++ b/src/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/Startup.fs b/src/Startup.fs similarity index 100% rename from Startup.fs rename to src/Startup.fs diff --git a/Ui.fs b/src/UI/Animate.fs similarity index 85% rename from Ui.fs rename to src/UI/Animate.fs index d2d5f90..0b66f9f 100644 --- a/Ui.fs +++ b/src/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/src/UI/Components.fs b/src/UI/Components.fs new file mode 100644 index 0000000..062b822 --- /dev/null +++ b/src/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/src/UI/LocalStorage.fs b/src/UI/LocalStorage.fs new file mode 100644 index 0000000..fda4262 --- /dev/null +++ b/src/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..5c7cdee 100644 --- a/web_api_cookbook.fsproj +++ b/web_api_cookbook.fsproj @@ -9,10 +9,14 @@ - - - - + + + + + + + + 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 +}