diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3b322d8..e6b1020 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['9.0.x'] + dotnet-version: ['10.0.x'] steps: - uses: actions/checkout@v2 diff --git a/Falco.Datastar.sln b/Falco.Datastar.sln index 4538d05..34d7628 100644 --- a/Falco.Datastar.sln +++ b/Falco.Datastar.sln @@ -18,6 +18,14 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Streaming", "Examples\Strea EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Falco.Datastar.Tests", "test\Falco.Datastar.Tests\Falco.Datastar.Tests.fsproj", "{772A0FC5-A796-4408-9751-53842F8B8C77}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "InputForm", "examples\InputForm\InputForm.fsproj", "{19E053A0-6EA6-4818-AF0D-5BE0B36D55EF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "environment", "environment", "{13C18205-082E-4DED-9AD4-D3001ABDFECA}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + .github\workflows\build.yml = .github\workflows\build.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,6 +38,7 @@ Global {8E2C0AA3-07E5-40D8-8F16-905F5F1D30AE} = {305B55B6-83D0-453C-A77E-080214FA0657} {0187E584-68DF-4D8C-874C-FF8E061932DF} = {305B55B6-83D0-453C-A77E-080214FA0657} {AECB950B-20DD-40C4-9ACA-3A8FF1CFBC05} = {305B55B6-83D0-453C-A77E-080214FA0657} + {19E053A0-6EA6-4818-AF0D-5BE0B36D55EF} = {305B55B6-83D0-453C-A77E-080214FA0657} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6BCE09E2-DA8E-44F9-B5EC-C4F76E329471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -64,5 +73,9 @@ Global {772A0FC5-A796-4408-9751-53842F8B8C77}.Debug|Any CPU.Build.0 = Debug|Any CPU {772A0FC5-A796-4408-9751-53842F8B8C77}.Release|Any CPU.ActiveCfg = Release|Any CPU {772A0FC5-A796-4408-9751-53842F8B8C77}.Release|Any CPU.Build.0 = Release|Any CPU + {19E053A0-6EA6-4818-AF0D-5BE0B36D55EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19E053A0-6EA6-4818-AF0D-5BE0B36D55EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19E053A0-6EA6-4818-AF0D-5BE0B36D55EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19E053A0-6EA6-4818-AF0D-5BE0B36D55EF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 2c4b1d6..2727181 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ Some important notes: Signals defined later in the DOM tree override those defin ## _Creating Signals_ Create signals, which are reactive variables that automatically propagate their value to all references of the signal. +Important: Never use hyphens when naming signals. ### [Ds.signals / Ds.signal : `data-signals`](https://data-star.dev/reference/attributes#data-signals) @@ -428,7 +429,6 @@ Each request action can also be provided a number of options, explained in depth ```fsharp Elem.button [ Ds.onClick (Ds.get ("/endpoint", { RequestOptions.Defaults with - IncludeLocal = true; Headers = [ ("X-Csrf-Token", "JImikTbsoCYQ9...") ] OpenWhenHidden = true } )) ] [ Text.raw "Push the Button" ] diff --git a/examples/InputForm/InputForm.fs b/examples/InputForm/InputForm.fs new file mode 100644 index 0000000..295a1d2 --- /dev/null +++ b/examples/InputForm/InputForm.fs @@ -0,0 +1,90 @@ +open Falco +open Falco.Datastar.Selector +open Falco.Datastar.SignalPath +open Falco.Markup +open Falco.Routing +open Falco.Datastar +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Http + +module View = + let template content = + Elem.html [ Attr.lang "en" ] [ + Elem.head [] [ Ds.cdnScript ] + Elem.body [] content + ] + +module App = + let handleIndex : HttpHandler = + let checkbox name = + [ Text.raw $"{name}:"; Elem.input [ Attr.type' "checkbox"; Attr.name "checkboxes"; Attr.value name ] ] + let html = + View.template [ + Text.h1 "Example: Input Form" + Elem.div [] [ + Elem.form [ Attr.id "myform" ] [ + yield! checkbox "foo" + yield! checkbox "bar" + yield! checkbox "baz" + Elem.button [ Ds.onClick (Ds.get("/endpoint1", RequestOptions.With(Form))) ] [ Text.raw "Submit GET Request" ] + Elem.button [ Ds.onClick (Ds.post("/endpoint1", RequestOptions.With(Form))) ] [ Text.raw "Submit POST Request" ] + ] + Elem.button [ Ds.onClick (Ds.get("/endpoint1", RequestOptions.With(SelectedForm (sel"#myform")))) ] [ + Text.raw "Submit GET request from outside the form" + ] + ] + Elem.hr [] + Elem.div [] [ + Elem.form [ Ds.onEvent ("submit", (Ds.post ("/endpoint2", RequestOptions.With(Form)))) ] [ + Text.raw "foo:" + Elem.input [ Attr.type' "text"; Attr.name "foo"; Attr.required ] + Elem.button [] [ Text.raw "Submit Form" ] + ] + ] + ] + Response.ofHtml html + + let handleEndpointOne (getForm:HttpContext -> RequestData) : HttpHandler = (fun ctx -> task { + let method = ctx.Request.Method + let form = ctx |> getForm + let foo = form.GetStringList("checkboxes") + + let alertString = $"Form data received via {method} request: checkboxes = {foo}" + let alertScript = $"alert('{alertString}')" + + return Response.ofExecuteScript alertScript ctx + }) + + let handleEndpointTwo (getForm:HttpContext -> RequestData): HttpHandler = (fun ctx -> task { + let method = ctx.Request.Method + let form = ctx |> getForm + let foo = form.GetString("foo") + + let alertString = $"Form data received via {method} request: foo = {foo}" + let alertScript = $"alert('{alertString}')" + + return Response.ofExecuteScript alertScript ctx + }) + + +[] +let main args = + let wapp = WebApplication.Create() + + let endpoints = + [ + get "/" App.handleIndex + all "/endpoint1" [ + GET, (App.handleEndpointOne Request.getQuery) + POST, (App.handleEndpointOne (fun ctx -> (ctx |> Request.getForm).Result)) + ] + all "/endpoint2" [ + GET, (App.handleEndpointTwo Request.getQuery) + POST, (App.handleEndpointTwo (fun ctx -> (ctx |> Request.getForm).Result)) + ] + ] + + wapp.UseRouting() + .UseFalco(endpoints) + .Run() + 0 // Exit code diff --git a/examples/InputForm/InputForm.fsproj b/examples/InputForm/InputForm.fsproj new file mode 100644 index 0000000..e298f1f --- /dev/null +++ b/examples/InputForm/InputForm.fsproj @@ -0,0 +1,19 @@ + + + net9.0 + + + + + + + + + + + + + PreserveNewest + + + diff --git a/examples/InputForm/appsettings.json b/examples/InputForm/appsettings.json new file mode 100644 index 0000000..2b4bc14 --- /dev/null +++ b/examples/InputForm/appsettings.json @@ -0,0 +1,8 @@ +{ + "Urls": "http://localhost:5001", + "Logging": { + "LogLevel": { + "Default": "Information" + } + } +} diff --git a/examples/Streaming/Animation.fs b/examples/Streaming/Animation.fs index 75b92a9..75756cb 100644 --- a/examples/Streaming/Animation.fs +++ b/examples/Streaming/Animation.fs @@ -40,7 +40,7 @@ let private totalBadAppleFrames = badAppleFrames |> Array.length backgroundTask { while true do currentBadAppleFrame <- (currentBadAppleFrame + 1) % totalBadAppleFrames - do! Task.Delay(TimeSpan.FromMilliseconds(50)) + do! Task.Delay(TimeSpan.FromMilliseconds(50L)) } |> ignore let getCurrentBadAppleFrame () = badAppleFrames[currentBadAppleFrame] diff --git a/examples/Streaming/Streaming.fs b/examples/Streaming/Streaming.fs index ff43572..be64546 100644 --- a/examples/Streaming/Streaming.fs +++ b/examples/Streaming/Streaming.fs @@ -36,7 +36,7 @@ let handleIndex ctx = task { Elem.html [] [ Elem.head [ Attr.title "Streaming" ] [ Ds.cdnScript - Elem.script [ Attr.type' "module"; Attr.src "datastar-inspector.js" ] [] + Elem.script [ Attr.type' "module" ] [] ] Elem.body [ Ds.signal (SignalPath.userName, user) @@ -62,8 +62,6 @@ let handleIndex ctx = task { Elem.label [ Attr.for' "streamDisplayGuids" ] [ Text.raw "Viewers" ] Elem.div [ Attr.id ElementIds.streamView ] [] - - Elem.create "datastar-inspector" [] [] ] ] return Response.ofHtml (html (Guid.NewGuid())) ctx @@ -110,7 +108,7 @@ let handleStream ctx = task { ] do! Response.sseHtmlElements ctx patch - do! Task.Delay (TimeSpan.FromSeconds(10L), ctx.RequestAborted) + do! Task.Delay (TimeSpan.FromMilliseconds(50), ctx.RequestAborted) finally userDisplays.AddOrUpdate (signalUser, UserState.displayBadApple, Func(fun _ _ -> UserState.loggedOff)) |> ignore return () diff --git a/global.json b/global.json new file mode 100644 index 0000000..ef68833 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.0", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/src/Falco.Datastar/Ds.fs b/src/Falco.Datastar/Ds.fs index 1a2cc97..e8e1634 100644 --- a/src/Falco.Datastar/Ds.fs +++ b/src/Falco.Datastar/Ds.fs @@ -28,7 +28,6 @@ type Ds = /// Signal is only merged if it doesn't already exist /// Attribute static member inline signal<'T> (signalPath:SignalPath, signalValue:'T, ?ifMissing) = - let zz = JsonSerializerOptions() DsAttr.start "signals" |> DsAttr.addSignalPathTarget signalPath |> DsAttr.addModifierNameIf "ifmissing" (defaultArg ifMissing false) @@ -61,7 +60,7 @@ type Ds = /// An HTML element attribute /// Expression to be evaluated and assigned to the attribute, https://data-star.dev/guide/datastar_expressions /// Attribute - static member attr' (attributeName, expression) = + static member inline attr' (attributeName, expression) = DsAttr.create ("attr", targetName = attributeName, value = expression) /// @@ -71,7 +70,7 @@ type Ds = /// /// The signal to bind to /// Attribute - static member bind signalPath = + static member inline bind signalPath = DsAttr.createSp ("bind", signalPath) /// @@ -92,7 +91,7 @@ type Ds = /// /// The style to set, https://www.w3schools.com/cssref/index.php /// Expression to be evaluated and assigned to the style property, https://data-star.dev/guide/datastar_expressions - static member style (styleProperty, propertyValueExpression) = + static member inline style (styleProperty, propertyValueExpression) = DsAttr.create ("style", targetName = styleProperty, value = propertyValueExpression) /// @@ -101,7 +100,7 @@ type Ds = /// /// Expression to be evaluated, https://data-star.dev/guide/datastar_expressions /// Attribute - static member text expression = + static member inline text expression = DsAttr.create ("text", value = expression) /// @@ -137,7 +136,7 @@ type Ds = /// /// The expression that will be evaluated; if true = the element is visible, https://data-star.dev/guide/datastar_expressions /// Attribute - static member show boolExpression = + static member inline show boolExpression = DsAttr.create ("show", value = boolExpression) /// @@ -145,7 +144,7 @@ type Ds = /// /// The expression to fire /// Attribute - static member effect (expression:string) = + static member inline effect (expression:string) = DsAttr.create ("effect", value = expression) /// @@ -177,7 +176,7 @@ type Ds = /// https://data-star.dev/reference/attributes#data-ignore /// /// Attribute - static member ignore = + static member inline ignore = DsAttr.create "ignore" /// @@ -198,14 +197,14 @@ type Ds = /// https://data-star.dev/reference/attributes#data-ignore-morph /// /// Attribute - static member ignoreMorph = + static member inline ignoreMorph = DsAttr.create "ignore-morph" /// /// Sets the text content of an element to a reactive JSON stringified version of signals. Useful for troubleshooting. /// https://data-star.dev/reference/attributes#data-json-signals /// - static member jsonSignals = + static member inline jsonSignals = DsAttr.create "json-signals" /// @@ -217,7 +216,7 @@ type Ds = static member jsonSignalsOptions (?signalsFilter:SignalsFilter, ?terse:bool) = let addSignalsFilter signalsFilter dsAttr = if signalsFilter = SignalsFilter.None - then dsAttr |> DsAttr.addValue (signalsFilter |> SignalsFilter.serialize) + then dsAttr |> DsAttr.addValue (signalsFilter |> SignalsFilter.Serialize) else dsAttr DsAttr.start "json-signals" |> DsAttr.addModifierNameIf "terse" (defaultArg terse false) @@ -261,7 +260,7 @@ type Ds = /// Attribute static member onInit (expression, ?delayMs, ?viewTransition) = DsAttr.start "init" - |> DsAttr.addModifierOption (delayMs |> Option.map DsAttrModifier.DelayMs) + |> DsAttr.addModifierOption (delayMs |> Option.toValueOption |> ValueOption.map DsAttrModifier.DelayMs) |> DsAttr.addModifierNameIf "viewtransition" (defaultArg viewTransition false) |> DsAttr.addValue expression |> DsAttr.create @@ -292,9 +291,9 @@ type Ds = /// Attribute static member onSignalPatch (expression, ?delayMs:int, ?debounce:Debounce, ?throttle:Throttle) = DsAttr.start "on-signal-patch" - |> DsAttr.addModifierOption (delayMs |> Option.map DsAttrModifier.DelayMs) - |> DsAttr.addModifierOption (debounce |> Option.map DsAttrModifier.Debounce) - |> DsAttr.addModifierOption (throttle |> Option.map DsAttrModifier.Throttle) + |> DsAttr.addModifierOption (delayMs |> Option.toValueOption |> ValueOption.map DsAttrModifier.DelayMs) + |> DsAttr.addModifierOption (debounce |> Option.toValueOption |> ValueOption.map DsAttrModifier.Debounce) + |> DsAttr.addModifierOption (throttle |> Option.toValueOption |> ValueOption.map DsAttrModifier.Throttle) |> DsAttr.addValue expression |> DsAttr.create @@ -306,7 +305,7 @@ type Ds = /// Attribute static member onSignalPatchFilter (signalsFilter:SignalsFilter) = DsAttr.start "on-signal-patch-filter" - |> DsAttr.addValue (signalsFilter |> SignalsFilter.serialize) + |> DsAttr.addValue (signalsFilter |> SignalsFilter.Serialize) |> DsAttr.create /// @@ -331,9 +330,9 @@ type Ds = ) |> DsAttr.addModifierNameIf "once" (defaultArg onlyOnce false) |> DsAttr.addModifierNameIf "viewtransition" (defaultArg viewTransition false) - |> DsAttr.addModifierOption (delayMs |> Option.map DsAttrModifier.DelayMs) - |> DsAttr.addModifierOption (debounce |> Option.map DsAttrModifier.Debounce) - |> DsAttr.addModifierOption (throttle |> Option.map DsAttrModifier.Throttle) + |> DsAttr.addModifierOption (delayMs |> Option.toValueOption |> ValueOption.map DsAttrModifier.DelayMs) + |> DsAttr.addModifierOption (debounce |> Option.toValueOption |> ValueOption.map DsAttrModifier.Debounce) + |> DsAttr.addModifierOption (throttle |> Option.toValueOption |> ValueOption.map DsAttrModifier.Throttle) |> DsAttr.addValue expression |> DsAttr.create @@ -342,16 +341,16 @@ type Ds = /// static member private backendAction actionOptions action = match (action, actionOptions) with - | Get url, None -> $@"@get('{url}')" - | Get url, Some options -> $"@get('{url}','{options |> RequestOptions.Serialize}')" - | Post url, None -> $@"@post('{url}')" - | Post url, Some options -> $"@post('{url}','{options |> RequestOptions.Serialize}')" - | Put url, None -> $@"@put('{url}')" - | Put url, Some options -> $"@put('{url}','{options |> RequestOptions.Serialize}')" - | Patch url, None -> $@"@patch('{url}')" - | Patch url, Some options -> $"@patch('{url}','{options |> RequestOptions.Serialize}')" - | Delete url, None -> $@"@delete('{url}')" - | Delete url, Some options -> $"@delete('{url}','{options |> RequestOptions.Serialize}')" + | Get url, ValueNone -> $@"@get('{url}')" + | Get url, ValueSome options -> $"@get('{url}',{options |> RequestOptions.Serialize})" + | Post url, ValueNone -> $@"@post('{url}')" + | Post url, ValueSome options -> $"@post('{url}',{options |> RequestOptions.Serialize})" + | Put url, ValueNone -> $@"@put('{url}')" + | Put url, ValueSome options -> $"@put('{url}',{options |> RequestOptions.Serialize})" + | Patch url, ValueNone -> $@"@patch('{url}')" + | Patch url, ValueSome options -> $"@patch('{url}',{options |> RequestOptions.Serialize})" + | Delete url, ValueNone -> $@"@delete('{url}')" + | Delete url, ValueSome options -> $"@delete('{url}',{options |> RequestOptions.Serialize})" /// /// Creates a @get action for an expression with options. The action sends a GET request with the given url. @@ -361,7 +360,7 @@ type Ds = /// /// Expression static member get (url, ?options) = - Ds.backendAction options (Get url) + Ds.backendAction (options |> Option.toValueOption) (Get url) /// /// Creates a @post action for an expression. The action sends a POST request to the given url. @@ -371,7 +370,7 @@ type Ds = /// /// Expression static member post (url, ?options) = - Ds.backendAction options (Post url) + Ds.backendAction (options |> Option.toValueOption) (Post url) /// /// Creates a @put action for an expression. The action sends a PUT request to the given url. @@ -381,7 +380,7 @@ type Ds = /// /// Expression static member put (url, ?options) = - Ds.backendAction options (Put url) + Ds.backendAction (options |> Option.toValueOption) (Put url) /// /// Creates a @patch action for an expression. The action sends a PATCH request to the given url. @@ -391,7 +390,7 @@ type Ds = /// /// Expression static member patch (url, ?options) = - Ds.backendAction options (Patch url) + Ds.backendAction (options |> Option.toValueOption) (Patch url) /// /// Creates a @delete action for an expression. The action sends a DELETE request to the given url. @@ -401,7 +400,7 @@ type Ds = /// /// Expression static member delete (url, ?options) = - Ds.backendAction options (Delete url) + Ds.backendAction (options |> Option.toValueOption) (Delete url) /// /// @setall(), set all the signals that start with the prefix to the expression provided. diff --git a/src/Falco.Datastar/Falco.Datastar.fsproj b/src/Falco.Datastar/Falco.Datastar.fsproj index d51bbd4..e04b7d2 100644 --- a/src/Falco.Datastar/Falco.Datastar.fsproj +++ b/src/Falco.Datastar/Falco.Datastar.fsproj @@ -1,7 +1,7 @@ Falco.Datastar - 1.1.0 + 1.2.0 Datastar Bindings for the Falco web toolkit. @@ -10,7 +10,7 @@ en-CA - net8.0;net9.0 + net8.0;net9.0;net10.0 embedded Library true @@ -19,7 +19,7 @@ Falco.Datastar - 1.1.0 + 1.2.0 fsharp;web;falco;falco-sharp;data-star https://github.com/falcoframework/Falco.Datastar Apache-2.0 @@ -39,7 +39,8 @@ - + + @@ -50,8 +51,5 @@ - - README.md - diff --git a/src/Falco.Datastar/Types.fs b/src/Falco.Datastar/Types.fs index 6bbf0d8..d49dd43 100644 --- a/src/Falco.Datastar/Types.fs +++ b/src/Falco.Datastar/Types.fs @@ -18,7 +18,7 @@ type SignalsFilter = static member None = { IncludePattern = ValueNone; ExcludePattern = ValueNone } static member Include pattern = { IncludePattern = ValueSome pattern; ExcludePattern = ValueNone } static member Exclude pattern = { IncludePattern = ValueNone; ExcludePattern = ValueSome pattern } - static member serialize (signalFilter:SignalsFilter) = + static member Serialize (signalFilter:SignalsFilter) = if signalFilter = SignalsFilter.None then "" else @@ -31,7 +31,7 @@ type SignalsFilter = | _ -> sb let _ = match signalFilter.ExcludePattern with - | ValueSome excludeExp -> sb.Append($"exclude: /{excludeExp}'") + | ValueSome excludeExp -> sb.Append($"exclude: /{excludeExp}/") | _ -> sb sb ) @@ -85,13 +85,53 @@ type BackendAction = | Delete of url:string type ContentType = + /// default, filtered signals; default | Json - | Form of string voption + /// sends a custom object instead of the default, filtered signals + | CustomJson of obj + /// validates inputs of closest form and sends them to the backend + | Form + /// similar to Form, but specify the form id to send + | SelectedForm of StarFederation.Datastar.FSharp.Selector + +type Retry = + /// retry on network errors; default + | Auto + /// retries on 4xx and 5xx responses + | Error + /// retries on all non-204 responses, except redirects + | Always + /// disables retry + | Never + +type RequestCancellation = + /// cancels existing requests on the same element; default + | Auto + /// allows concurrent requests + | Disabled + /// an object name that can be aborted; https://data-star.dev/reference/actions#request-cancellation; + /// creator should include '$', e.g. (AbortController "$controller") + | AbortController of string + with + static member Serialize (requestCancellation:RequestCancellation) = + match requestCancellation with + | Auto -> "auto" + | Disabled -> "disabled" + | AbortController controller -> controller + +type ResponseOverrideMode = + | Outer + | Inner + | Remove + | Replace + | Prepend + | Append + | Before + | After /// Request Options for backend action plugins /// https://data-star.dev/reference/action_plugins -type RequestOptions = - { +type RequestOptions = { /// The type of content to send. A value of json sends all signals in a JSON request. /// A value of form tells the action to look for the closest form to the element on which it is placed /// (unless a selector option is provided), perform validation on the form elements, @@ -101,17 +141,16 @@ type RequestOptions = /// Filter object utilizing regular expressions for which signals to send FilterSignals: SignalsFilter - /// Specifies a form to send when the ContentType is set to Form. - /// If set to ValueNone, the closest form is used. Defaults to ValueNone. - Selector: Selector voption - /// HTTP Headers to send with the request. - Headers: (string*string) list + Headers: (string * string) list /// Whether to keep the connection open when the page is hidden. Useful for dashboards /// but can cause a drain on battery life and other resources when enabled. Defaults to false. OpenWhenHidden: bool + /// Determines on what to retry; auto, error, always, never + Retry: Retry + /// The retry interval in milliseconds. Defaults to 1 second RetryInterval: TimeSpan @@ -125,38 +164,41 @@ type RequestOptions = RetryMaxCount: int /// An AbortSignal object that can be used to cancel the request. - /// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal - Abort: obj } - + /// https://data-star.dev/reference/actions#request-cancellation + RequestCancellation: RequestCancellation + } + with static member Defaults = { ContentType = Json FilterSignals = SignalsFilter.None - Selector = ValueNone Headers = [] OpenWhenHidden = false + Retry = Retry.Auto RetryInterval = TimeSpan.FromSeconds(1.0) RetryScaler = 2.0 RetryMaxWait = TimeSpan.FromSeconds(30.0) RetryMaxCount = 10 - Abort = null } + RequestCancellation = Auto } + + static member inline With contentType = { RequestOptions.Defaults with ContentType = contentType } static member internal Serialize (backendActionOptions:RequestOptions) = let jsonObject = JsonObject() + match backendActionOptions.ContentType with | _ when backendActionOptions.ContentType = RequestOptions.Defaults.ContentType -> () - | Json -> jsonObject.Add("contentType", "json") - | Form formSelector -> + | Form -> jsonObject.Add("contentType", "form") + | SelectedForm formSelector -> jsonObject.Add("contentType", "form") - match formSelector with - | ValueNone -> () - | ValueSome formSelector' -> jsonObject.Add("selector", formSelector') - - if backendActionOptions.FilterSignals <> SignalsFilter.None then - jsonObject.Add("includeLocal", backendActionOptions.FilterSignals |> SignalsFilter.serialize |> JsonNode.Parse) + jsonObject.Add("selector", formSelector) + | CustomJson customJson -> + let serializedOverride = JsonSerializer.Serialize(customJson, JsonSerializerOptions.SignalsDefault) + jsonObject.Add("contentType", "json") + jsonObject.Add("override", serializedOverride) + | Json -> jsonObject.Add("contentType", "json") - if backendActionOptions.Selector.IsValueSome then - let selector = backendActionOptions.Selector |> ValueOption.get - jsonObject.Add("selector", selector) + if backendActionOptions.FilterSignals <> RequestOptions.Defaults.FilterSignals then + jsonObject.Add("filterSignals", backendActionOptions.FilterSignals |> SignalsFilter.Serialize |> JsonNode.Parse) if backendActionOptions.Headers.Length > 0 then let headerObject = JsonObject() @@ -178,8 +220,9 @@ type RequestOptions = if backendActionOptions.RetryMaxCount <> RequestOptions.Defaults.RetryMaxCount then jsonObject.Add("retryMaxCount", backendActionOptions.RetryMaxCount) - if backendActionOptions.Abort <> null then - jsonObject.Add("abort", JsonSerializer.Serialize backendActionOptions.Abort) + if backendActionOptions.RequestCancellation <> RequestOptions.Defaults.RequestCancellation then + let requestCancellation = backendActionOptions.RequestCancellation |> RequestCancellation.Serialize + jsonObject.Add("requestCancellation", requestCancellation) let options = JsonSerializerOptions() options.WriteIndented <- false @@ -191,7 +234,7 @@ type Debounce = NoTrailing:bool } static member inline With (timeSpan:TimeSpan, ?leading:bool, ?noTrailing:bool) = { TimeSpan = timeSpan; Leading = (defaultArg leading false); NoTrailing = (defaultArg noTrailing false) } - static member inline With (milliseconds:int, ?leading:bool, ?noTrailing:bool) = + static member inline With (milliseconds:float, ?leading:bool, ?noTrailing:bool) = { TimeSpan = TimeSpan.FromMilliseconds(milliseconds); Leading = (defaultArg leading false); NoTrailing = (defaultArg noTrailing false) } type Throttle = @@ -200,7 +243,7 @@ type Throttle = Trailing:bool } static member inline With (timeSpan:TimeSpan, ?noLeading:bool, ?trailing:bool) = { TimeSpan = timeSpan; NoLeading = (defaultArg noLeading false); Trailing = (defaultArg trailing false) } - static member inline With (milliseconds:int, ?noLeading:bool, ?trailing:bool) = + static member inline With (milliseconds:float, ?noLeading:bool, ?trailing:bool) = { TimeSpan = TimeSpan.FromMilliseconds(milliseconds); NoLeading = (defaultArg noLeading false); Trailing = (defaultArg trailing false) } type OnEventModifier = @@ -311,8 +354,8 @@ type DsAttr = static member inline addModifierOption modifierOption dsAttr = match modifierOption with - | Some modifier -> DsAttr.addModifier modifier dsAttr - | None -> dsAttr + | ValueSome modifier -> DsAttr.addModifier modifier dsAttr + | ValueNone -> dsAttr static member inline addModifierName modifierName = DsAttr.addModifier { Name = modifierName; Tags = [] } diff --git a/src/Falco.Datastar/Utility.fs b/src/Falco.Datastar/Utility.fs index 5767583..bd9b755 100644 --- a/src/Falco.Datastar/Utility.fs +++ b/src/Falco.Datastar/Utility.fs @@ -2,8 +2,6 @@ namespace Falco.Datastar open System open System.Text -open System.Text.Json -open StarFederation.Datastar.FSharp module internal String = let newLines = [| "\r\n"; "\n"; "\r" |] @@ -23,3 +21,8 @@ module internal Bool = match bool with | true -> trueThing | _ -> falseThing + +module Option = + let toValueOption = function + | Some value -> ValueSome value + | None -> ValueNone diff --git a/test/Falco.Datastar.Tests/DsTests.fs b/test/Falco.Datastar.Tests/DsTests.fs index ccf8b21..2b94d99 100644 --- a/test/Falco.Datastar.Tests/DsTests.fs +++ b/test/Falco.Datastar.Tests/DsTests.fs @@ -16,3 +16,18 @@ module DsTests = let ``Ds.bind should create an attribute`` () = testElem [ Ds.bind "signalPath" ] |> should equal """
div
""" + + [] + let ``Ds.post`` () = + Ds.post "/channel" + |> should equal """@post('/channel')""" + + [] + let ``Ds.post with Form `` () = + Ds.post ("/channel", { RequestOptions.Defaults with ContentType = Form }) + |> should equal """@post('/channel',{"contentType":"form"})""" + + [] + let ``Ds.post with SelectedForm `` () = + Ds.post ("/channel", { RequestOptions.Defaults with ContentType = (SelectedForm "myForm") }) + |> should equal """@post('/channel',{"contentType":"form","selector":"myForm"})"""