From 0fcb40fbdd65eb2bb9174c86600d8e7ccb85bc6b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 12 Jul 2026 14:08:32 -0400 Subject: [PATCH] WIP on htmx 4; user messages now work (#60) --- src/MyWebLog/Handlers/Helpers.fs | 24 ++++++++---------------- src/MyWebLog/Views/User.fs | 4 ++-- src/MyWebLog/Views/WebLog.fs | 14 ++++++++------ src/admin-theme/wwwroot/admin.js | 17 ++++++++--------- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index 79c22ad..b0e3402 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -107,7 +107,7 @@ let isHtmx (ctx: HttpContext) = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh /// Convert messages to headers (used for htmx responses) -let messagesToHeaders (messages: UserMessage array) pushUrl : HttpHandler = +let messagesToHeaders (messages: UserMessage array) : HttpHandler = seq { yield! messages @@ -116,10 +116,7 @@ let messagesToHeaders (messages: UserMessage array) pushUrl : HttpHandler = | Some detail -> $"{m.Level}|||{m.Message}|||{detail}" | None -> $"{m.Level}|||{m.Message}" |> setHttpHeader "X-Message") - match pushUrl with - | Some true -> withHxPushUrl "true" - | Some false -> withHxNoPushUrl - | None -> () + withHxNoPushUrl } |> Seq.reduce (>=>) @@ -158,7 +155,7 @@ module Error = { UserMessage.Error with Message = $"You are not authorized to access the URL {ctx.Request.Path.Value}" } |] - (messagesToHeaders messages (Some false) >=> setStatusCode 401) earlyReturn ctx + (messagesToHeaders messages >=> setStatusCode 401) earlyReturn ctx else setStatusCode 401 earlyReturn ctx /// Handle 404s @@ -168,14 +165,14 @@ module Error = let messages = [| { UserMessage.Error with Message = $"The URL {ctx.Request.Path.Value} was not found" } |] - RequestErrors.notFound (messagesToHeaders messages (Some false)) earlyReturn ctx + RequestErrors.notFound (messagesToHeaders messages) earlyReturn ctx else RequestErrors.NOT_FOUND "Not found" earlyReturn ctx) let server message : HttpHandler = handleContext (fun ctx -> if isHtmx ctx then let messages = [| { UserMessage.Error with Message = message } |] - ServerErrors.internalError (messagesToHeaders messages (Some false)) earlyReturn ctx + ServerErrors.internalError (messagesToHeaders messages) earlyReturn ctx else ServerErrors.INTERNAL_ERROR message earlyReturn ctx) @@ -215,7 +212,7 @@ let bareForTheme themeId template next ctx viewCtx = task { match! Template.Cache.get themeId "layout-bare" ctx.Data with | Ok layoutTemplate -> return! - (messagesToHeaders completeCtx.Messages (Some false) + (messagesToHeaders completeCtx.Messages >=> htmlString (Template.render layoutTemplate completeCtx ctx.Data)) next ctx | Error message -> return! Error.server message next ctx | Error message -> return! Error.server message next ctx @@ -231,12 +228,7 @@ let adminPage pageTitle next ctx (content: AppViewContext -> XmlNode list) = tas let! messages = getCurrentMessages ctx let appCtx = generateViewContext messages (viewCtxForPage pageTitle) ctx let layout = if isHtmx ctx then Layout.partial else Layout.full - let returnFunc html = - if isHtmx ctx && messages.Length > 0 then - messagesToHeaders messages None >=> htmlString html - else - htmlString html - return! (layout content appCtx |> RenderView.AsString.htmlDocument |> returnFunc) next ctx + return! (layout content appCtx |> RenderView.AsString.htmlDocument |> htmlString) next ctx } /// Display a bare page for an admin endpoint @@ -244,7 +236,7 @@ let adminBarePage pageTitle next ctx (content: AppViewContext -> XmlNode list) = let! messages = getCurrentMessages ctx let appCtx = generateViewContext messages (viewCtxForPage pageTitle) ctx return! - ( messagesToHeaders appCtx.Messages (Some false) + (messagesToHeaders appCtx.Messages >=> htmlString (Layout.bare content appCtx |> RenderView.AsString.htmlDocument)) next ctx } diff --git a/src/MyWebLog/Views/User.fs b/src/MyWebLog/Views/User.fs index 9166784..fd8df5c 100644 --- a/src/MyWebLog/Views/User.fs +++ b/src/MyWebLog/Views/User.fs @@ -13,7 +13,7 @@ let edit (model: EditUserModel) app = div [ _class "col-12" ] [ h5 [ _class "my-3" ] [ txt app.PageTitle ] form [ _hxPost (relUrl app "admin/settings/user/save"); _method "post"; _class "container" - _hxTarget "#user_panel"; _hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [ + _hxTarget "#user_panel"; _hxSwap $"{HxSwap.OuterHtml} show:top showTarget:window" ] [ antiCsrf app input [ _type "hidden"; _name "Id"; _value model.Id ] div [ _class "row" ] [ @@ -97,7 +97,7 @@ let edit (model: EditUserModel) app = let logOn (model: LogOnModel) (app: AppViewContext) = [ h2 [ _class "my-3" ] [ rawText "Log On to "; encodedText app.WebLog.Name ] article [ _class "py-3" ] [ - form [ _action (relUrl app "user/log-on"); _method "post"; _class "container"; _hxPushUrl "true" ] [ + form [ _action (relUrl app "user/log-on"); _method "post"; _hxBoost; _class "container" ] [ antiCsrf app if Option.isSome model.ReturnTo then input [ _type "hidden"; _name "ReturnTo"; _value model.ReturnTo.Value ] div [ _class "row" ] [ diff --git a/src/MyWebLog/Views/WebLog.fs b/src/MyWebLog/Views/WebLog.fs index 671314f..daf305d 100644 --- a/src/MyWebLog/Views/WebLog.fs +++ b/src/MyWebLog/Views/WebLog.fs @@ -70,8 +70,8 @@ let categoryList includeNew app = [ a [ _href (relUrl app $"category/{cat.Slug}"); _target "_blank" ] [ raw $"View { cat.PostCount} Post"; if cat.PostCount <> 1 then raw "s" ]; actionSpacer - a [ _href $"{catUrl}/edit"; _hxTarget $"#cat_{cat.Id}" - _hxSwap $"{HxSwap.InnerHtml} show:#cat_{cat.Id}:top" ] [ + a [ _href $"{catUrl}/edit"; _hxBoost; _hxTarget $"#cat_{cat.Id}" + _hxSwap $"{HxSwap.InnerHtml} show:top showTarget:#cat_{cat.Id}" ] [ raw "Edit" ]; actionSpacer a [ _href catUrl; _hxDelete catUrl; _hxTarget "body"; _class "text-danger" @@ -85,11 +85,13 @@ let categoryList includeNew app = [ ] ] let loadNew = - span [ _hxGet (relUrl app "admin/category/new/edit"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml ] [] + span [ _hxGet (relUrl app "admin/category/new/edit"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml + _hxTarget "this" ] [] h2 [ _class "my-3" ] [ raw app.PageTitle ] article [] [ - a [ _href (relUrl app "admin/category/new/edit"); _class "btn btn-primary btn-sm mb-3"; _hxTarget "#cat_new" ] [ + a [ _href (relUrl app "admin/category/new/edit"); _hxBoost; _class "btn btn-primary btn-sm mb-3" + _hxTarget "#cat_new" ] [ raw "Add a New Category" ] div [ _id "catList"; _class "container" ] [ @@ -118,7 +120,7 @@ let categoryList includeNew app = [ /// The main dashboard let dashboard (model: DashboardModel) app = [ h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " • Dashboard" ] - article [ _class "container" ] [ + article [ _class "container"; hxInherited _hxBoost ] [ div [ _class "row" ] [ section [ _class "col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3" ] [ div [ _class "card" ] [ @@ -193,7 +195,7 @@ let dashboard (model: DashboardModel) app = [ let feedEdit (model: EditCustomFeedModel) (ratings: MetaItem list) (mediums: MetaItem list) app = [ h2 [ _class "my-3" ] [ raw app.PageTitle ] article [] [ - form [ _action (relUrl app "admin/settings/rss/save"); _method "post"; _class "container" ] [ + form [ _action (relUrl app "admin/settings/rss/save"); _method "post"; _hxBoost; _class "container" ] [ antiCsrf app input [ _type "hidden"; _name "Id"; _value model.Id ] div [ _class "row pb-3" ] [ diff --git a/src/admin-theme/wwwroot/admin.js b/src/admin-theme/wwwroot/admin.js index df9a0d7..1c8eb8c 100644 --- a/src/admin-theme/wwwroot/admin.js +++ b/src/admin-theme/wwwroot/admin.js @@ -442,22 +442,21 @@ this.Admin = { } } -htmx.on("htmx:afterOnLoad", function (evt) { - const hdrs = evt.detail.xhr.getAllResponseHeaders() +htmx.on("htmx:after:swap", function (evt) { // Initialize any toasts that were pre-rendered from the server Admin.showPreRenderedMessages() // Show messages if there were any in the response - if (hdrs.indexOf("x-message") >= 0) { - Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message")) + const hdrs = evt.detail.ctx.response.headers + if (hdrs.has("x-message")) { + hdrs.get("x-message").split(", ").forEach(Admin.showMessage) } }) -htmx.on("htmx:responseError", function (evt) { - const xhr = evt.detail.xhr - const hdrs = xhr.getAllResponseHeaders() +htmx.on("htmx:response:error", function (evt) { + const resp = evt.detail.ctx.response // Show an error message if there were none in the response - if (hdrs.indexOf("x-message") < 0) { - Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`) + if (!resp.headers.has("x-message")) { + Admin.showMessage(`danger|||${resp.status}: ${resp.statusText}`) } })