Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.9.8 #4

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas": {
"version": "6.2.3",
"commands": [
"fantomas"
]
}
}
}
6 changes: 5 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ insert_final_newline=true
indent_style=space
indent_size=4
max_line_length=120
tab_width=4
tab_width=4

[*.{fs,fsx}]
fsharp_array_or_list_multiline_formatter = number_of_items
fsharp_multiline_bracket_style = stroustrup
9 changes: 9 additions & 0 deletions Falco.Htmx.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{637DC4AC-C
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Falco.Htmx.Tests", "test\Falco.Htmx.Tests\Falco.Htmx.Tests.fsproj", "{CF9948AD-3B91-4504-AACB-4B8EEB4EA148}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{086ACD53-E718-4281-B364-39F63AF493A1}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "HelloWorld", "samples\HelloWorld\HelloWorld.fsproj", "{AB2EFD5B-F371-4AAE-A964-08A6FB5397B0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -28,9 +32,14 @@ Global
{CF9948AD-3B91-4504-AACB-4B8EEB4EA148}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF9948AD-3B91-4504-AACB-4B8EEB4EA148}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF9948AD-3B91-4504-AACB-4B8EEB4EA148}.Release|Any CPU.Build.0 = Release|Any CPU
{AB2EFD5B-F371-4AAE-A964-08A6FB5397B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB2EFD5B-F371-4AAE-A964-08A6FB5397B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB2EFD5B-F371-4AAE-A964-08A6FB5397B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB2EFD5B-F371-4AAE-A964-08A6FB5397B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3CA77A41-A75F-47F8-AD54-4BADE9BBE528} = {BA616886-B858-409E-87B3-8C58ED6F8150}
{CF9948AD-3B91-4504-AACB-4B8EEB4EA148} = {637DC4AC-CD12-459E-9F22-DE299EC5F977}
{AB2EFD5B-F371-4AAE-A964-08A6FB5397B0} = {086ACD53-E718-4281-B364-39F63AF493A1}
EndGlobalSection
EndGlobal
1 change: 0 additions & 1 deletion samples/HelloWorld/HelloWorld.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Falco.Htmx\Falco.Htmx.fsproj" />
Expand Down
61 changes: 26 additions & 35 deletions samples/HelloWorld/Program.fs
Original file line number Diff line number Diff line change
@@ -1,71 +1,62 @@
module HelloWorld.Program

open Falco
open Falco
open Falco.Markup
open Falco.Routing
open Falco.HostBuilder
open Falco.Htmx

module Components =
let notClickedElement =
Elem.div [ Attr.id "click_indicator" ] [
Text.raw "Not clicked :-(" ]
Elem.div [ Attr.id "click_indicator" ] [ Text.raw "Not clicked :-(" ]

let clickMeElement =
Elem.div [
Attr.style "width: 40px; text-align: center; margin-bottom: 5px; border: 1px black solid; cursor: pointer;"
Hx.post "/click"
Hx.swap Hx.Swap.innerHTML
Hx.target (Hx.Target.css "#click_indicator")
(*Hx.trigger [ Hx.Triggers.click (Hx.Triggers.Modifiers.once) ] *) ] [
Elem.div [
Attr.id "clicker"
Hx.target (Hx.Target.css "#clicker")
Hx.post "/change-to-reset-button"
(*Hx.trigger [Hx.Triggers.fromResponse (Hx.Triggers.ResponseEvent ("change-to-reset-button"))]*) ] [
Text.raw ("Click me!") ] ]
] [
Elem.div [
Attr.id "clicker"
Hx.target (Hx.Target.css "#clicker")
Hx.post "/change-to-reset-button"
] [ Text.raw ("Click me!") ]
]

let clickSection =
Elem.div [ Attr.id "click-section" ] [
clickMeElement
notClickedElement ]
notClickedElement
]

let clickedElement =
Elem.div [ Attr.id "click_indicator" ] [ Text.raw "Clicked!" ]
let clickedElement = Elem.div [ Attr.id "click_indicator" ] [ Text.raw "Clicked!" ]

let resetButton =
Elem.div [
Attr.id ("clicker")
Hx.target (Hx.Target.css "#click-section")
Hx.post "/reset"
(* Hx.trigger [Hx.Triggers.click (Hx.Triggers.Modifiers.once)] *) ] [
Text.raw ("Reset") ]
] [ Text.raw ("Reset") ]

let handleHtml : HttpHandler =
let handleHtml: HttpHandler =
let html =
Templates.html5 "en"
[
Elem.link [ Attr.href "style.css"; Attr.rel "stylesheet" ]
Elem.script [ Attr.src Script.src ] []
]
[
Elem.h1 [] [
Text.raw "Hello from Falco.Htmx" ]
Components.clickSection
Templates.html5 "en" [
Elem.link [
Attr.href "style.css"
Attr.rel "stylesheet"
]
Elem.script [ Attr.src Script.src ] []
] [
Elem.h1 [] [ Text.raw "Hello from Falco.Htmx" ]
Components.clickSection
]

Response.ofHtml html

let handleClick : HttpHandler =
Response.withHxTrigger ("change-to-reset-button", None)
>> Response.ofHtml Components.clickedElement

let handleClick: HttpHandler = Response.ofHtml Components.clickedElement

let handlerReset : HttpHandler =
Response.ofHtml Components.clickSection
let handlerReset: HttpHandler = Response.ofHtml Components.clickSection

let handleChangeToResetButton : HttpHandler =
Response.ofHtml Components.resetButton
let handleChangeToResetButton: HttpHandler = Response.ofHtml Components.resetButton


[<EntryPoint>]
Expand Down
100 changes: 53 additions & 47 deletions src/Falco.Htmx/Extensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@ open Falco
open Microsoft.AspNetCore.Http

/// htmx Request Headers
type HtmxRequestHeaders =
{ /// Indicates that the request is via an element using hx-boost
HxBoosted : string option
/// The current URL of the browser
HxCurrentUrl : string option
/// True if the request is for history restoration after a miss in the local history cache
HxHistoryRestoreRequest : string option
/// The user response to an hx-prompt
HxPrompt : string option
/// Always true
HxRequest : string option
/// The id of the target element if it exists
HxTarget : string option
/// The name of the triggered element if it exists
HxTriggerName : string option
/// The id of the triggered element if it exists
HxTrigger : string option }
type HtmxRequestHeaders = {
/// Indicates that the request is via an element using hx-boost
HxBoosted: string option
/// The current URL of the browser
HxCurrentUrl: string option
/// True if the request is for history restoration after a miss in the local history cache
HxHistoryRestoreRequest: string option
/// The user response to an hx-prompt
HxPrompt: string option
/// Always true
HxRequest: string option
/// The id of the target element if it exists
HxTarget: string option
/// The name of the triggered element if it exists
HxTriggerName: string option
/// The id of the triggered element if it exists
HxTrigger: string option
}

/// Value for the HX-Trigger Response Header
type HxTriggerResponse =
Expand All @@ -29,52 +30,57 @@ type HxTriggerResponse =

[<RequireQualifiedAccess>]
module Request =
let getHtmxHeaders (ctx : HttpContext) : HtmxRequestHeaders =
let getHtmxHeaders (ctx: HttpContext) : HtmxRequestHeaders =
let headers = Request.getHeaders ctx

{ HxBoosted = headers.TryGet "HX-Boosted"
HxCurrentUrl = headers.TryGet "HX-Current-URL"
HxHistoryRestoreRequest = headers.TryGet "HX-History-Restore-Request"
HxPrompt = headers.TryGet "HX-Prompt"
HxRequest = headers.TryGet "HX-Request"
HxTarget = headers.TryGet "HX-Target"
HxTriggerName = headers.TryGet "HX-Trigger-Name"
HxTrigger = headers.TryGet "HX-Trigger" }
{
HxBoosted = headers.TryGet "HX-Boosted"
HxCurrentUrl = headers.TryGet "HX-Current-URL"
HxHistoryRestoreRequest = headers.TryGet "HX-History-Restore-Request"
HxPrompt = headers.TryGet "HX-Prompt"
HxRequest = headers.TryGet "HX-Request"
HxTarget = headers.TryGet "HX-Target"
HxTriggerName = headers.TryGet "HX-Trigger-Name"
HxTrigger = headers.TryGet "HX-Trigger"
}

type AjaxContext(?event, ?source, ?handler, ?target, ?swap, ?values, ?headers) =
/// The source element of the request
member _.Source : TargetOption option = source
member _.Source: TargetOption option = source
/// An event that "triggered" the request
member _.Event : string option = event
member _.Event: string option = event
/// A callback that will handle the response HTML
member _.Handler : string option = handler
member _.Handler: string option = handler
/// The target to swap the response into
member _.Target : TargetOption option = target
member _.Target: TargetOption option = target
/// How the response will be swapped in relative to the target
member _.Swap : SwapOption option = swap
member _.Swap: SwapOption option = swap
/// Values to submit with the request
member _.Values : (string * string) list = defaultArg values []
member _.Values: (string * string) list = defaultArg values []
/// Headers to submit with the request
member _.Headers : (string * string) list = defaultArg headers []
member _.Headers: (string * string) list = defaultArg headers []

[<RequireQualifiedAccess>]
module Response =
open System.Text.Json

let [<Literal>] private _trueValue = "true"
[<Literal>]
let private _trueValue = "true"

// Allows you to do a client-side redirect that does not do a full page reload
let withHxLocation (path : string, ctx : AjaxContext option) : HttpResponseModifier =
let withHxLocation (path: string, ctx: AjaxContext option) : HttpResponseModifier =
let headerValue =
match ctx with
| None -> path
| Some ctx' ->
[ "path", path
"source", Option.map TargetOption.AsString ctx'.Source |> Option.defaultValue ""
"event", Option.defaultValue "" ctx'.Event
"handler", Option.defaultValue "" ctx'.Handler
"target", Option.map TargetOption.AsString ctx'.Target |> Option.defaultValue ""
"swap", Option.map SwapOption.AsString ctx'.Swap |> Option.defaultValue "" ]
[
"path", path
"source", Option.map TargetOption.AsString ctx'.Source |> Option.defaultValue ""
"event", Option.defaultValue "" ctx'.Event
"handler", Option.defaultValue "" ctx'.Handler
"target", Option.map TargetOption.AsString ctx'.Target |> Option.defaultValue ""
"swap", Option.map SwapOption.AsString ctx'.Swap |> Option.defaultValue ""
]
|> Map.ofList
|> fun x -> JsonSerializer.Serialize(x, Json.defaultSerializerOptions)

Expand All @@ -89,23 +95,23 @@ module Response =
Response.withHeaders [ "HX-Redirect", url ]

// If set to "true" the client side will do a a full refresh of the page
let withHxRefresh : HttpResponseModifier =
let withHxRefresh: HttpResponseModifier =
Response.withHeaders [ "HX-Refresh", _trueValue ]

// Replaces the current URL in the location bar
let withHxReplaceUrl (url: string) : HttpResponseModifier =
Response.withHeaders [ "HX-Replace-Url", url ]

// Allows you to specify how the response will be swapped. See hx-swap for possible values
let withHxReswap (option : SwapOption) =
let withHxReswap (option: SwapOption) =
Response.withHeaders [ "HX-Reswap", SwapOption.AsString option ]

// A CSS selector that updates the target of the content update to a different element on the page
let withHxRetarget (option : TargetOption) =
let withHxRetarget (option: TargetOption) =
Response.withHeaders [ "HX-Retarget", TargetOption.AsString option ]

// Allows you to trigger client side events, see the documentation for more info
let withHxTrigger<'T>(triggerResponse: HxTriggerResponse) : HttpResponseModifier =
let withHxTrigger<'T> (triggerResponse: HxTriggerResponse) : HttpResponseModifier =
let headerValue =
match triggerResponse with
| Events events -> events |> String.concat ", "
Expand All @@ -117,11 +123,11 @@ module Response =
Response.withHeaders [ "HX-Trigger", headerValue ]

// Allows you to trigger client side events, see the documentation for more info
let withHxTriggerAfterSettle : HttpResponseModifier =
let withHxTriggerAfterSettle: HttpResponseModifier =
Response.withHeaders [ "HX-Trigger-After-Settle", _trueValue ]

// Allows you to trigger client side events, see the documentation for more info
let withHxTriggerAfterSwap : HttpResponseModifier =
let withHxTriggerAfterSwap: HttpResponseModifier =
Response.withHeaders [ "HX-Trigger-After-Swap", _trueValue ]

/// A CSS selector that allows you to choose which part of the response is used to be swapped in.
Expand Down
Loading
Loading