Skip to content

Commit 8a1f972

Browse files
committed
Merge branch 'develop'
2 parents edd1648 + 5c368ff commit 8a1f972

17 files changed

Lines changed: 307 additions & 40 deletions

File tree

examples/Basic/Basic.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="Oxpecker" Version="2.1.1" />
1111
<PackageReference Include="Oxpecker.OpenApi" Version="2.1.2" />
12-
<PackageReference Include="Oxpecker.Htmx" Version="4.0.0-beta.5" />
12+
<PackageReference Include="Oxpecker.Htmx" Version="4.0.0" />
1313
<PackageReference Include="FSharp.Control.TaskSeq" Version="1.1.1" />
1414
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
1515
</ItemGroup>

examples/Basic/Program.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ let streamingHtml1: EndpointHandler =
118118
fun (ctx: HttpContext) ->
119119
let html =
120120
html() {
121-
head() { script(src = "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta4") }
121+
head() { script(src = "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0") }
122122
body(style = "width: 800px; margin: 0 auto") {
123123
h1(style = "text-align: center; color: blue") { "HTML Streaming example" }
124124
h2().hxGet("/streamHtml2").hxTarget(HxSelector.this').hxTrigger("load")

examples/ContactApp/ContactApp.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<ItemGroup>
3333
<PackageReference Include="Oxpecker" Version="2.1.1" />
3434
<PackageReference Include="Oxpecker.ViewEngine" Version="2.1.0" />
35-
<PackageReference Include="Oxpecker.Htmx" Version="4.0.0-beta.5" />
35+
<PackageReference Include="Oxpecker.Htmx" Version="4.0.0" />
3636
</ItemGroup>
3737

3838
</Project>

examples/ContactApp/templates/shared/layout.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module layout =
1414
html(lang="") {
1515
head() {
1616
title() { "Contact App" }
17-
script(src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta4",
17+
script(src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0",
1818
crossorigin="anonymous")
1919
link(rel="stylesheet", href="/site.css")
2020
}

examples/WeatherApp/WeatherApp.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<ItemGroup>
2222
<PackageReference Include="Oxpecker" Version="2.1.1" />
2323
<PackageReference Include="Oxpecker.ViewEngine" Version="2.1.0" />
24-
<PackageReference Include="Oxpecker.Htmx" Version="4.0.0-beta.5" />
24+
<PackageReference Include="Oxpecker.Htmx" Version="4.0.0" />
2525
<PackageReference Include="Oxpecker.Alpine" Version="0.3.0" />
2626
</ItemGroup>
2727

examples/WeatherApp/templates/shared/layout.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ module layout =
8383
link(rel="stylesheet", href="/bootstrap/bootstrap.min.css")
8484
link(rel="stylesheet", href="/app.css")
8585
link(rel="icon", type'="image/png", href="/favicon.png")
86-
script(src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta4", crossorigin="anonymous")
86+
script(src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0", crossorigin="anonymous")
8787
script(defer=true, src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.11/dist/cdn.min.js", crossorigin="anonymous")
8888
}
8989
body().hxBoost("true", HxModifier.inherited) {

src/Oxpecker.Htmx/Attributes.fs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ type HtmxVerbExtensions =
2727
[<Extension>]
2828
static member hxDelete(this: #HtmlTag, [<StringSyntax("Uri")>] url: string | null) = this.attr("hx-delete", url)
2929

30+
/// Issues a QUERY to the specified URL. QUERY is a safe, idempotent method (like GET) that
31+
/// sends its content in the request body (like POST).
32+
[<Extension>]
33+
static member hxQuery(this: #HtmlTag, [<StringSyntax("Uri")>] url: string | null) = this.attr("hx-query", url)
34+
3035

3136
/// Event handler attribute.
3237
type HtmxEventExtensions =
@@ -255,10 +260,19 @@ type HtmxAdditionalExtensions =
255260
[<Extension>]
256261
static member hxIgnore(this: #HtmlTag, value: bool) = this.bool("hx-ignore", value)
257262

258-
/// Show optimistic content during request (template id, e.g. `#my-template`).
263+
/// Leaves an element untouched during a morph swap: neither its attributes nor its children
264+
/// are morphed. Useful for third party widgets, web components and elements holding
265+
/// client-side state or animations. Renders `hx-morph-skip` (boolean attribute) when true.
266+
/// Applies to morph swaps only.
267+
[<Extension>]
268+
static member hxMorphSkip(this: #HtmlTag, value: bool) = this.bool("hx-morph-skip", value)
269+
270+
/// Morphs the attributes of an element during a morph swap, but leaves its children untouched.
271+
/// Renders `hx-morph-skip-children` (boolean attribute) when true.
272+
/// Applies to morph swaps only.
259273
[<Extension>]
260-
static member hxOptimistic(this: #HtmlTag, [<StringSyntax("css")>] value: string | null) =
261-
this.attr("hx-optimistic", value)
274+
static member hxMorphSkipChildren(this: #HtmlTag, value: bool) =
275+
this.bool("hx-morph-skip-children", value)
262276

263277
/// Handle responses differently by status code (`hx-status:CODE`).
264278
/// `code` is an exact code (e.g. "404"), single-digit wildcard (e.g. "50x"),

src/Oxpecker.Htmx/Extensions.fs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ module HxUpsertModifier =
3333
/// `key:<attr>` — sort by a different attribute than `id`.
3434
let key (attr: string) = $" key:%s{attr}"
3535

36+
/// Special binding names for `hxLiveBind` (`hx-live:{name}`) provided by the `hx-live` extension.
37+
/// Any other name binds the attribute or property of that name.
38+
/// See https://four.htmx.org/extensions/hx-live
39+
[<RequireQualifiedAccess>]
40+
module HxLiveBinding =
41+
/// `text` — bind the element's `textContent` to the expression.
42+
[<Literal>]
43+
let text = "text"
44+
/// `html` — bind the element's `innerHTML` to the expression.
45+
[<Literal>]
46+
let html = "html"
47+
/// `style` — bind inline styles; the expression returns a `"prop: value; …"` string or an object of declarations.
48+
[<Literal>]
49+
let style = "style"
50+
/// `class` — bind classes; the expression returns a class name or an object mapping class names to booleans.
51+
[<Literal>]
52+
let class' = "class"
53+
/// `.{name}` — toggle the single class `name` on the truthiness of the expression.
54+
let inline toggleClass (name: string) = $".%s{name}"
55+
3656

3757
/// Attributes provided by the built-in htmx 4 extensions.
3858
/// Each extension must be loaded client-side via its own script tag.
@@ -52,6 +72,20 @@ type HtmxExtensionExtensions =
5272
[<Extension>]
5373
static member hxSseClose(this: #HtmlTag, event: string | null) = this.attr("hx-sse:close", event)
5474

75+
// ─── hx-multipart (multipart/mixed + multipart/parallel streaming) ───
76+
77+
/// `hx-multipart:connect` — open a persistent GET request that streams `multipart/mixed` parts,
78+
/// reconnecting automatically. Requires the `hx-multipart` extension.
79+
[<Extension>]
80+
static member hxMultipartConnect(this: #HtmlTag, [<StringSyntax("Uri")>] url: string | null) =
81+
this.attr("hx-multipart:connect", url)
82+
83+
/// `hx-multipart:close` — an `hx-trigger`-style event spec: when that event fires on the element the
84+
/// persistent multipart connection is closed. The server can fire it from a part via an `HX-Trigger`
85+
/// header. Requires the `hx-multipart` extension.
86+
[<Extension>]
87+
static member hxMultipartClose(this: #HtmlTag, event: string | null) = this.attr("hx-multipart:close", event)
88+
5589
// ─── hx-ws (WebSockets) ───
5690

5791
/// `hx-ws:connect` — establish a WebSocket connection to the URL.
@@ -70,6 +104,39 @@ type HtmxExtensionExtensions =
70104
[<Extension>]
71105
static member hxWsSend(this: #HtmlTag, [<StringSyntax("Uri")>] url: string | null) = this.attr("hx-ws:send", url)
72106

107+
// ─── hx-prompt (browser prompt before request) ───
108+
109+
/// `hx-prompt` — show a browser prompt before the request; the answer is sent URI-encoded as the
110+
/// `HX-Prompt` request header (decode it with `Uri.UnescapeDataString`) and cancelling aborts the
111+
/// request. Inheritable. Requires the `hx-prompt` extension.
112+
[<Extension>]
113+
static member hxPrompt(this: #HtmlTag, question: string | null) = this.attr("hx-prompt", question)
114+
115+
/// `hx-prompt` — show a browser prompt before the request; the answer is sent URI-encoded as the
116+
/// `HX-Prompt` request header (decode it with `Uri.UnescapeDataString`) and cancelling aborts the
117+
/// request. Inheritable. Requires the `hx-prompt` extension.
118+
[<Extension>]
119+
static member hxPrompt(this: #HtmlTag, question: string | null, modifiers: string) =
120+
this.attr($"hx-prompt%s{modifiers}", question)
121+
122+
// ─── hx-pending (content shown while a request is in flight) ───
123+
124+
/// `hx-pending` — CSS selector of a `<template>` (or element) whose content is shown while the
125+
/// request is in flight. The inserted wrapper gets the `hx-pending` class and a `data-*`
126+
/// attribute per request parameter. Inheritable.
127+
/// Requires the `hx-pending` extension.
128+
[<Extension>]
129+
static member hxPending(this: #HtmlTag, [<StringSyntax("css")>] value: string | null) =
130+
this.attr("hx-pending", value)
131+
132+
/// `hx-pending` — CSS selector of a `<template>` (or element) whose content is shown while the
133+
/// request is in flight. The inserted wrapper gets the `hx-pending` class and a `data-*`
134+
/// attribute per request parameter. Inheritable.
135+
/// Requires the `hx-pending` extension.
136+
[<Extension>]
137+
static member hxPending(this: #HtmlTag, [<StringSyntax("css")>] value: string | null, modifiers: string) =
138+
this.attr($"hx-pending%s{modifiers}", value)
139+
73140
// ─── hx-head (head merging) ───
74141

75142
/// `hx-head` — control how the response `<head>` is merged (`merge`, `append`, or `re-eval`).
@@ -127,3 +194,11 @@ type HtmxExtensionExtensions =
127194
[<Extension>]
128195
static member hxLive(this: #HtmlTag, [<StringSyntax("js")>] expression: string | null) =
129196
this.attr("hx-live", expression)
197+
198+
/// `hx-live:{name}` — bind an attribute or property to a reactive JavaScript expression that is
199+
/// re-evaluated whenever the DOM changes, e.g. `hxLiveBind("disabled", "!q('#name').value")`.
200+
/// `name` is usually an attribute or property name; see `HxLiveBinding` for the special names
201+
/// `text`, `html`, `style`, `class` and `.{class}`. Requires the `hx-live` extension.
202+
[<Extension>]
203+
static member hxLiveBind(this: #HtmlTag, name: string, [<StringSyntax("js")>] expression: string | null) =
204+
this.attr($"hx-live:{name}", expression)

src/Oxpecker.Htmx/Headers.fs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ module HxRequestHeader =
3838
/// `HX-PTag` — polling tag sent back to the server by the `hx-ptag` extension.
3939
[<Literal>]
4040
let PTag = "HX-PTag"
41-
/// `HX-Request-ID`unique request/response correlation id sent by the `hx-ws` extension in the message headers (echoed back by the server).
41+
/// `HX-Prompt`the user's answer to the `hx-prompt` dialog, sent URI-encoded by the `hx-prompt` extension (decode it with `Uri.UnescapeDataString`).
4242
[<Literal>]
43-
let RequestId = "HX-Request-ID"
43+
let Prompt = "HX-Prompt"
44+
/// `HX-Last-Part-ID` — the `HX-Part-ID` of the last completed part, sent by the `hx-multipart` extension when reconnecting so the server can resume the stream.
45+
[<Literal>]
46+
let LastPartId = "HX-Last-Part-ID"
4447

4548
[<RequireQualifiedAccess>]
4649
module HxResponseHeader =
@@ -78,6 +81,6 @@ module HxResponseHeader =
7881
/// `HX-PTag` — polling tag stored by the `hx-ptag` extension and echoed on the next request.
7982
[<Literal>]
8083
let PTag = "HX-PTag"
81-
/// `HX-Request-ID`the request/response correlation id echoed back by the server so the `hx-ws` extension can route the response to the originating element.
84+
/// `HX-Part-ID`per-part identifier set on a `multipart/mixed` part by the server, remembered by the `hx-multipart` extension and sent back as `HX-Last-Part-ID` on reconnect.
8285
[<Literal>]
83-
let RequestId = "HX-Request-ID"
86+
let PartId = "HX-Part-ID"

src/Oxpecker.Htmx/Helpers.fs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module HxModifier =
1919
/// Builders for extended selectors.
2020
/// See https://four.htmx.org/docs/features/extended-selectors
2121
/// Each helper returns the htmx selector string ready to pass into selector-typed attributes
22-
/// such as `hxTarget`, `hxSelect`, `hxSelectOob`, `hxIndicator`, `hxInclude`, `hxDisable`, `hxOptimistic`.
22+
/// such as `hxTarget`, `hxSelect`, `hxSelectOob`, `hxIndicator`, `hxInclude`, `hxDisable`.
2323
[<RequireQualifiedAccess>]
2424
module HxSelector =
2525
/// `this` — the element itself.
@@ -112,9 +112,9 @@ module HxSwapModifier =
112112
/// `swapMs` — adds delay before swap, in milliseconds.
113113
let inline swapMs value = $" swap:%i{value}ms"
114114
/// `settle` — adds delay between the swap and the settle phase.
115-
let inline settle value = $"settle:%s{value}"
115+
let inline settle value = $" settle:%s{value}"
116116
/// `settleMs` — adds delay between the swap and the settle phase, in milliseconds.
117-
let inline settleMs value = $"settle:%i{value}ms"
117+
let inline settleMs value = $" settle:%i{value}ms"
118118
/// `ignoreTitle` — prevents htmx from updating the document title based on the response.
119119
let inline ignoreTitle value = $" ignoreTitle:%b{value}"
120120
/// `scroll` — auto-scroll to swapped content.
@@ -129,6 +129,10 @@ module HxSwapModifier =
129129
let inline target value = $" target:%s{value}"
130130
/// `strip` — controls whether the outer element of the response content is removed before swapping.
131131
let inline strip value = $" strip:%b{value}"
132+
/// `swapEmpty` — controls whether the main swap still runs when the response is empty after
133+
/// out-of-band and partial content has been removed. Defaults to `false` when the response
134+
/// contained partials.
135+
let inline swapEmpty value = $" swapEmpty:%b{value}"
132136

133137
/// Synthetic event names for `hxTrigger`, beyond the standard DOM events.
134138
[<RequireQualifiedAccess>]

0 commit comments

Comments
 (0)