diff --git a/sln/src/Docs/docs/datastar.md b/sln/src/Docs/docs/datastar.md index 018d86c..62f9a88 100644 --- a/sln/src/Docs/docs/datastar.md +++ b/sln/src/Docs/docs/datastar.md @@ -12,19 +12,6 @@ open type Html open type Datastar ``` -## Generic Attribute - -### data-* - -Use `_ds` for any Datastar `data-*` attribute: - -```fsharp -div { - _ds ("star", "true") - _ds "loading" -} -``` - ## Core Attributes ### data-signals @@ -33,8 +20,8 @@ Define reactive signals on an element: ```fsharp div { - _dsSignals ("count", "0") - _dsSignals ("name", "'World'") + _dataSignals ("count", "0") + _dataSignals ("name", "'World'") } ``` @@ -44,7 +31,7 @@ Listen for events and run expressions: ```fsharp button { - _dsOn ("click", "$count++") + _dataOn ("click", "$count++") "Increment" } ``` @@ -54,8 +41,7 @@ button { Two-way bind a signal to an input element: ```fsharp -input { _type "text"; _dsBind "name" } -input { _type "text"; _dsBind ("name", "value") } +input { _type "text"; _dataBind "name" } ``` ### data-show @@ -63,7 +49,7 @@ input { _type "text"; _dsBind ("name", "value") } Conditionally show or hide an element: ```fsharp -div { _dsShow "$count > 0"; "Count is positive" } +div { _dataShow "$count > 0"; "Count is positive" } ``` ### data-text @@ -71,7 +57,7 @@ div { _dsShow "$count > 0"; "Count is positive" } Set the text content of an element reactively: ```fsharp -span { _dsText "$count" } +span { _dataText "$count" } ``` ### data-effect @@ -79,7 +65,7 @@ span { _dsText "$count" } Run an expression whenever its dependencies change: ```fsharp -div { _dsEffect "console.log($count)" } +div { _dataEffect "console.log($count)" } ``` ### data-init @@ -87,7 +73,7 @@ div { _dsEffect "console.log($count)" } Run an expression when the element is initialized: ```fsharp -div { _dsInit "console.log('initialized')" } +div { _dataInit "console.log('initialized')" } ``` ### data-attr @@ -95,7 +81,7 @@ div { _dsInit "console.log('initialized')" } Dynamically set an HTML attribute: ```fsharp -div { _dsAttr ("disabled", "$count === 0") } +div { _dataAttr ("disabled", "$count === 0") } ``` ### data-class @@ -103,7 +89,7 @@ div { _dsAttr ("disabled", "$count === 0") } Toggle a CSS class based on an expression: ```fsharp -div { _dsClass ("active", "$isActive") } +div { _dataClass ("active", "$isActive") } ``` ### data-computed @@ -111,7 +97,7 @@ div { _dsClass ("active", "$isActive") } Define a computed signal derived from other signals: ```fsharp -div { _dsComputed ("double", "$count * 2") } +div { _dataComputed ("double", "$count * 2") } ``` ### data-style @@ -119,7 +105,7 @@ div { _dsComputed ("double", "$count * 2") } Dynamically set a CSS style property: ```fsharp -div { _dsStyle ("color", "$isError ? 'red' : 'green'") } +div { _dataStyle ("color", "$isError ? 'red' : 'green'") } ``` ### data-ref @@ -127,8 +113,7 @@ div { _dsStyle ("color", "$isError ? 'red' : 'green'") } Reference an element by name: ```fsharp -input { _dsRef "myInput" } -input { _dsRef ("myInput", "value") } +input { _dataRef "myInput" } ``` ### data-indicator @@ -136,17 +121,16 @@ input { _dsRef ("myInput", "value") } Bind a loading indicator signal: ```fsharp -button { _dsIndicator "loading" } -button { _dsIndicator ("loading", "true") } +button { _dataIndicator "loading" } ``` ### data-json-signals -Merge JSON signals into the signal store: +Render signals as JSON for debugging: ```fsharp -div { _dsJsonSignals """{"count": 0}""" } -div { _dsJsonSignals () } +pre { _dataJsonSignals () } +pre { _dataJsonSignals "{include: /counter/, exclude: /temp$/}" } ``` ### data-ignore @@ -154,7 +138,7 @@ div { _dsJsonSignals () } Prevent Datastar from processing an element: ```fsharp -div { _dsIgnore } +div { _dataIgnore } ``` ### data-ignore-morph @@ -162,7 +146,7 @@ div { _dsIgnore } Prevent morphing of an element during updates: ```fsharp -div { _dsIgnoreMorph } +div { _dataIgnoreMorph } ``` ### data-on-intersect @@ -170,7 +154,7 @@ div { _dsIgnoreMorph } Run an expression when an element enters the viewport: ```fsharp -div { _dsOnIntersect "$count++" } +div { _dataOnIntersect "$count++" } ``` ### data-on-interval @@ -178,7 +162,7 @@ div { _dsOnIntersect "$count++" } Run an expression on a timed interval: ```fsharp -div { _dsOnInterval "$count++" } +div { _dataOnInterval "$count++" } ``` ### data-on-signal-patch @@ -186,7 +170,7 @@ div { _dsOnInterval "$count++" } Run an expression when signals are patched: ```fsharp -div { _dsOnSignalPatch "console.log('patched')" } +div { _dataOnSignalPatch "console.log('patched')" } ``` ### data-on-signal-patch-filter @@ -194,7 +178,7 @@ div { _dsOnSignalPatch "console.log('patched')" } Filter which signal patches trigger the expression: ```fsharp -div { _dsOnSignalPatchFilter "count" } +div { _dataOnSignalPatchFilter "{include: /^count$/}" } ``` ### data-preserve-attr @@ -202,7 +186,7 @@ div { _dsOnSignalPatchFilter "count" } Preserve specified attributes during morphing: ```fsharp -div { _dsPreserveAttr "class" } +div { _dataPreserveAttr "class" } ``` ## Pro Attributes @@ -212,7 +196,7 @@ div { _dsPreserveAttr "class" } Apply animations to an element: ```fsharp -div { _dsAnimate "fadeIn 0.5s" } +div { _dataAnimate "fadeIn 0.5s" } ``` ### data-custom-validity @@ -220,7 +204,7 @@ div { _dsAnimate "fadeIn 0.5s" } Set custom validation messages: ```fsharp -input { _dsCustomValidity "$name === '' ? 'Name is required' : ''" } +input { _dataCustomValidity "$name === '' ? 'Name is required' : ''" } ``` ### data-on-raf @@ -228,7 +212,7 @@ input { _dsCustomValidity "$name === '' ? 'Name is required' : ''" } Run an expression on every animation frame: ```fsharp -canvas { _dsOnRaf "draw()" } +canvas { _dataOnRaf "draw()" } ``` ### data-on-resize @@ -236,16 +220,17 @@ canvas { _dsOnRaf "draw()" } Run an expression when the element is resized: ```fsharp -div { _dsOnResize "console.log('resized')" } +div { _dataOnResize "console.log('resized')" } ``` ### data-persist -Persist signals to local storage: +Persist signals to local storage (or session storage with modifiers): ```fsharp -div { _dsPersist "count" } -div { _dsPersist ("count", "session") } +div { _dataPersist () } // default key: datastar +div { _dataPersist "mykey" } // custom storage key +div { _dataPersist ("mykey", "{include: /foo/}") } // key + filter object ``` ### data-query-string @@ -253,8 +238,8 @@ div { _dsPersist ("count", "session") } Sync signals with URL query parameters: ```fsharp -div { _dsQueryString "count" } -div { _dsQueryString () } +div { _dataQueryString () } +div { _dataQueryString "{include: /foo/, exclude: /temp$/}" } ``` ### data-replace-url @@ -262,15 +247,15 @@ div { _dsQueryString () } Replace the current URL: ```fsharp -div { _dsReplaceUrl "/new-path" } +div { _dataReplaceUrl "/new-path" } ``` ### data-rocket -Prefetch pages for instant navigation: +Create a Rocket web component: ```fsharp -a { _dsRocket "true"; _href "/next-page"; "Next" } +div { _dataRocket "{ endpoint: '/stream' }" } ``` ### data-scroll-into-view @@ -278,7 +263,7 @@ a { _dsRocket "true"; _href "/next-page"; "Next" } Scroll the element into view: ```fsharp -div { _dsScrollIntoView } +div { _dataScrollIntoView } ``` ### data-view-transition @@ -286,7 +271,7 @@ div { _dsScrollIntoView } Apply view transitions: ```fsharp -div { _dsViewTransition "fade" } +div { _dataViewTransition "fade" } ``` ## Complete Example @@ -295,19 +280,19 @@ Here's a complete example combining multiple Datastar attributes: ```fsharp div { - _dsSignals ("count", "0") - _dsSignals ("name", "'World'") - _dsComputed ("greeting", "'Hello, ' + $name + '!'") + _dataSignals ("count", "0") + _dataSignals ("name", "'World'") + _dataComputed ("greeting", "'Hello, ' + $name + '!'") - input { _type "text"; _dsBind "name" } - span { _dsText "$greeting" } + input { _type "text"; _dataBind "name" } + span { _dataText "$greeting" } button { - _dsOn ("click", "$count++") - _dsClass ("active", "$count > 0") + _dataOn ("click", "$count++") + _dataClass ("active", "$count > 0") "Clicked " } - span { _dsText "$count" } - span { _dsShow "$count > 0"; " times" } + span { _dataText "$count" } + span { _dataShow "$count > 0"; " times" } } ``` diff --git a/sln/src/FSharp.ViewEngine/Datastar.fs b/sln/src/FSharp.ViewEngine/Datastar.fs index 258a260..4f910c5 100644 --- a/sln/src/FSharp.ViewEngine/Datastar.fs +++ b/sln/src/FSharp.ViewEngine/Datastar.fs @@ -1,45 +1,42 @@ namespace FSharp.ViewEngine type Datastar = - // Generic data-* attribute - static member inline _ds (key: string, value: string) = { Name = $"data-{key}"; Value = ValueSome value } - static member inline _ds (key: string) = { Name = $"data-{key}"; Value = ValueNone } - // Core attributes - static member inline _dsAttr (name: string, v: string) = { Name = $"data-attr:{name}"; Value = ValueSome v } - static member inline _dsBind (name: string) = { Name = $"data-bind:{name}"; Value = ValueNone } - static member inline _dsBind (name: string, v: string) = { Name = $"data-bind:{name}"; Value = ValueSome v } - static member inline _dsClass (name: string, v: string) = { Name = $"data-class:{name}"; Value = ValueSome v } - static member inline _dsComputed (name: string, v: string) = { Name = $"data-computed:{name}"; Value = ValueSome v } - static member inline _dsEffect (v: string) = { Name = "data-effect"; Value = ValueSome v } - static member inline _dsIgnore = { Name = "data-ignore"; Value = ValueNone } - static member inline _dsIgnoreMorph = { Name = "data-ignore-morph"; Value = ValueNone } - static member inline _dsIndicator (name: string) = { Name = $"data-indicator:{name}"; Value = ValueNone } - static member inline _dsIndicator (name: string, v: string) = { Name = $"data-indicator:{name}"; Value = ValueSome v } - static member inline _dsInit (v: string) = { Name = "data-init"; Value = ValueSome v } - static member inline _dsJsonSignals (?v: string) = match v with Some v -> { Name = "data-json-signals"; Value = ValueSome v } | None -> { Name = "data-json-signals"; Value = ValueNone } - static member inline _dsOn (event: string, v: string) = { Name = $"data-on:{event}"; Value = ValueSome v } - static member inline _dsOnIntersect (v: string) = { Name = "data-on-intersect"; Value = ValueSome v } - static member inline _dsOnInterval (v: string) = { Name = "data-on-interval"; Value = ValueSome v } - static member inline _dsOnSignalPatch (v: string) = { Name = "data-on-signal-patch"; Value = ValueSome v } - static member inline _dsOnSignalPatchFilter (v: string) = { Name = "data-on-signal-patch-filter"; Value = ValueSome v } - static member inline _dsPreserveAttr (v: string) = { Name = "data-preserve-attr"; Value = ValueSome v } - static member inline _dsRef (name: string) = { Name = $"data-ref:{name}"; Value = ValueNone } - static member inline _dsRef (name: string, v: string) = { Name = $"data-ref:{name}"; Value = ValueSome v } - static member inline _dsShow (v: string) = { Name = "data-show"; Value = ValueSome v } - static member inline _dsSignals (name: string, v: string) = { Name = $"data-signals:{name}"; Value = ValueSome v } - static member inline _dsStyle (prop: string, v: string) = { Name = $"data-style:{prop}"; Value = ValueSome v } - static member inline _dsText (v: string) = { Name = "data-text"; Value = ValueSome v } + static member inline _dataAttr (name: string, v: string) = { Name = $"data-attr:{name}"; Value = ValueSome v } + static member inline _dataBind (name: string) = { Name = $"data-bind:{name}"; Value = ValueNone } + static member inline _dataBind (name: string, v: string) = { Name = $"data-bind:{name}"; Value = ValueSome v } + static member inline _dataClass (name: string, v: string) = { Name = $"data-class:{name}"; Value = ValueSome v } + static member inline _dataComputed (name: string, v: string) = { Name = $"data-computed:{name}"; Value = ValueSome v } + static member inline _dataEffect (v: string) = { Name = "data-effect"; Value = ValueSome v } + static member inline _dataIgnore = { Name = "data-ignore"; Value = ValueNone } + static member inline _dataIgnoreMorph = { Name = "data-ignore-morph"; Value = ValueNone } + static member inline _dataIndicator (name: string) = { Name = $"data-indicator:{name}"; Value = ValueNone } + static member inline _dataIndicator (name: string, v: string) = { Name = $"data-indicator:{name}"; Value = ValueSome v } + static member inline _dataInit (v: string) = { Name = "data-init"; Value = ValueSome v } + static member inline _dataJsonSignals (?v: string) = match v with Some v -> { Name = "data-json-signals"; Value = ValueSome v } | None -> { Name = "data-json-signals"; Value = ValueNone } + static member inline _dataOn (event: string, v: string) = { Name = $"data-on:{event}"; Value = ValueSome v } + static member inline _dataOnIntersect (v: string) = { Name = "data-on-intersect"; Value = ValueSome v } + static member inline _dataOnInterval (v: string) = { Name = "data-on-interval"; Value = ValueSome v } + static member inline _dataOnSignalPatch (v: string) = { Name = "data-on-signal-patch"; Value = ValueSome v } + static member inline _dataOnSignalPatchFilter (v: string) = { Name = "data-on-signal-patch-filter"; Value = ValueSome v } + static member inline _dataPreserveAttr (v: string) = { Name = "data-preserve-attr"; Value = ValueSome v } + static member inline _dataRef (name: string) = { Name = $"data-ref:{name}"; Value = ValueNone } + static member inline _dataRef (name: string, v: string) = { Name = $"data-ref:{name}"; Value = ValueSome v } + static member inline _dataShow (v: string) = { Name = "data-show"; Value = ValueSome v } + static member inline _dataSignals (name: string, v: string) = { Name = $"data-signals:{name}"; Value = ValueSome v } + static member inline _dataStyle (prop: string, v: string) = { Name = $"data-style:{prop}"; Value = ValueSome v } + static member inline _dataText (v: string) = { Name = "data-text"; Value = ValueSome v } // Pro attributes - static member inline _dsAnimate (v: string) = { Name = "data-animate"; Value = ValueSome v } - static member inline _dsCustomValidity (v: string) = { Name = "data-custom-validity"; Value = ValueSome v } - static member inline _dsOnRaf (v: string) = { Name = "data-on-raf"; Value = ValueSome v } - static member inline _dsOnResize (v: string) = { Name = "data-on-resize"; Value = ValueSome v } - static member inline _dsPersist (key: string) = { Name = $"data-persist:{key}"; Value = ValueNone } - static member inline _dsPersist (key: string, v: string) = { Name = $"data-persist:{key}"; Value = ValueSome v } - static member inline _dsQueryString (?v: string) = match v with Some v -> { Name = "data-query-string"; Value = ValueSome v } | None -> { Name = "data-query-string"; Value = ValueNone } - static member inline _dsReplaceUrl (v: string) = { Name = "data-replace-url"; Value = ValueSome v } - static member inline _dsRocket (v: string) = { Name = "data-rocket"; Value = ValueSome v } - static member inline _dsScrollIntoView = { Name = "data-scroll-into-view"; Value = ValueNone } - static member inline _dsViewTransition (v: string) = { Name = "data-view-transition"; Value = ValueSome v } + static member inline _dataAnimate (v: string) = { Name = "data-animate"; Value = ValueSome v } + static member inline _dataCustomValidity (v: string) = { Name = "data-custom-validity"; Value = ValueSome v } + static member inline _dataOnRaf (v: string) = { Name = "data-on-raf"; Value = ValueSome v } + static member inline _dataOnResize (v: string) = { Name = "data-on-resize"; Value = ValueSome v } + static member inline _dataPersist () = { Name = "data-persist"; Value = ValueNone } + static member inline _dataPersist (key: string) = { Name = $"data-persist:{key}"; Value = ValueNone } + static member inline _dataPersist (key: string, v: string) = { Name = $"data-persist:{key}"; Value = ValueSome v } + static member inline _dataQueryString (?v: string) = match v with Some v -> { Name = "data-query-string"; Value = ValueSome v } | None -> { Name = "data-query-string"; Value = ValueNone } + static member inline _dataReplaceUrl (v: string) = { Name = "data-replace-url"; Value = ValueSome v } + static member inline _dataRocket (v: string) = { Name = "data-rocket"; Value = ValueSome v } + static member inline _dataScrollIntoView = { Name = "data-scroll-into-view"; Value = ValueNone } + static member inline _dataViewTransition (v: string) = { Name = "data-view-transition"; Value = ValueSome v } diff --git a/sln/src/Tests/Tests.fs b/sln/src/Tests/Tests.fs index 097b0d2..ecae26d 100644 --- a/sln/src/Tests/Tests.fs +++ b/sln/src/Tests/Tests.fs @@ -9,6 +9,7 @@ open Expecto open type Html open type Htmx open type Alpine +open type Datastar open type Svg open type Tailwind @@ -145,6 +146,50 @@ let tests = Expect.equal (String.clean actual) (String.clean expectedHtml) "Rendered HTML should match expected" } + test "Datastar attributes should render with data- prefix" { + let actual = + div { + _dataSignals ("count", "0") + _dataOn ("click", "$count++") + _dataShow "$count > 0" + _dataText "$count" + _dataBind "name" + _dataEffect "console.log($count)" + _dataClass ("active", "$isActive") + _dataAttr ("disabled", "$count === 0") + _dataComputed ("double", "$count * 2") + _dataInit "console.log('init')" + _dataIgnore + _dataIgnoreMorph + _dataStyle ("color", "red") + _dataRef "myInput" + _dataIndicator "loading" + _dataAnimate "fadeIn" + _dataPersist () + _dataPersist "count" + _dataScrollIntoView + "Content" + } |> Render.toString + Expect.stringContains actual "data-signals:count=\"0\"" "data-signals" + Expect.stringContains actual "data-on:click=\"$count++\"" "data-on" + Expect.stringContains actual "data-show=\"$count > 0\"" "data-show" + Expect.stringContains actual "data-text=\"$count\"" "data-text" + Expect.stringContains actual "data-bind:name" "data-bind" + Expect.stringContains actual "data-effect=\"console.log($count)\"" "data-effect" + Expect.stringContains actual "data-class:active=\"$isActive\"" "data-class" + Expect.stringContains actual "data-attr:disabled=\"$count === 0\"" "data-attr" + Expect.stringContains actual "data-computed:double=\"$count * 2\"" "data-computed" + Expect.stringContains actual "data-init=\"console.log('init')\"" "data-init" + Expect.stringContains actual "data-ignore-morph" "data-ignore-morph" + Expect.stringContains actual "data-ignore" "data-ignore" + Expect.stringContains actual "data-style:color=\"red\"" "data-style" + Expect.stringContains actual "data-ref:myInput" "data-ref" + Expect.stringContains actual "data-indicator:loading" "data-indicator" + Expect.stringContains actual "data-animate=\"fadeIn\"" "data-animate" + Expect.stringContains actual "data-persist data-persist:count" "data-persist" + Expect.stringContains actual "data-scroll-into-view" "data-scroll-into-view" + } + test "HtmlEncode should match HttpUtility.HtmlEncode" { let inputs = [