Fork me on GitHub
#biff
<
2024-01-04
>
chromalchemy19:01:57

Can anyone guide me or have and example of what the back and forth would be to submit a form -> validate fields server side -> if invalid, update current form html + state -> if valid, run server fn, then update current view or build new page with confirmation info. I have accomplished this with regular biff htmx example (using {_:hx-post_ "/demo/set-bar" _:hx-swap_ "outerHTML"}) . Now I need to do the same with submitting form with a captcha (submitting form with js fn). I haven’t tried using the htmx js api yet. I thought I might be missing something more idiomatic first. I am open to not using htmx and just building new pages, but then where would the intermediate invalid form view code/logic go? In general I’m having some trouble understanding flow between :post and :get routes/handlers. If form sends a :post, how to properly update view?. If form sends a :get, where to put validation logic/loop? Probably not really biff issues. I’m just an html client/server noob.

chromalchemy19:01:54

Here’s a stripped down demo. https://github.com/chromalchemy/groundedsol/blob/contact-form/src/gs/groundedsol/demo.clj First form posts, but doesn’t update. Second form has js callback, but goes to new raw page.

Jacob O'Bryant22:01:03

There are a couple ways to do it--with or without htmx. If you don't use htmx, you can have the form submit a regular POST request to your backend endpoint. If there are errors, that endpoint redirects back to the original page and includes error codes etc in the query params. Something like this:

(defn handle-form-submission [{:keys [params] :as ctx}]
  (let [error (cond
                (empty? (:name params)) "invalid-name"
                ...)]
    (when-not error
      (do-something ctx))
    (if error
      {:status 303
       :headers {"location" (str "/my-page?error=" error)}}
      {:status 303
       :headers {"location" "/next-page"}})))
If you're using htmx to submit the request, the structure is similar. But instead of returning a {:status 303 ...} thing, you'd render the form immediately (with error messages) and return that. Or if it was successful, you'd replace the form with a success message or something:
(defn my-form [{:keys [::error] :as ctx}]
  [:div
   (case error
     "invalid-name" [:div "that name is invalid"]
     nil)
   (biff/form ...)])

(defn handle-form-submission [{:keys [params] :as ctx}]
  (let [error (cond
                (empty? (:name params)) "invalid-name"
                ...)]
    (when-not error
      (do-something ctx))
    (if error
      (my-form (assoc ctx))
      (success-message ctx))))
If you want to return a completely new page on success instead of just replacing the form, looks like HX-Redirect (https://htmx.org/reference/#response_headers) might be the easiest way. I think you'd use it like this:
(defn handle-form-submission [{:keys [params] :as ctx}]
  (let [error (cond
                (empty? (:name params)) "invalid-name"
                ...)]
    (when-not error
      (do-something ctx))
    (if error
      (my-form (assoc ctx))
      {:status 200
       :headers {"HX-Redirect" "/next-page"}})))

chromalchemy20:01:16

@U7YNGKDHA Thank you for these patterns! They helped me orient to how to structure form server-side validation code via the handler fn and passing errors. I got an more evolved demo working just right! but without recaptcha and using :hx-swap and :hx-post directly on form… But when I go to enable re-captcha, I’m back to square 1, not knowing how to update form in place. 😭 Recaptcha is supposed to call the embeded js callback. Which should handle the update. If my callback is

function submitContact(token) { document.getElementById('contact-form').submit();} 
And I just have an _:action_ "/send-contact" on the form. Then the submit loads , and displays the form raw, without any page context. I tried using this htmx ajax fn, instead of the above .submit
function submitContact(token) { htmx.ajax('POST', '/send-contact', {target:'#contact-form', swap:'outerHTML'});
}
But it doesn’t seem to work , and gives me errors like this.
POST  405 (Method Not Allowed)

[email protected]:1 Response Status Error Code 405 from /send-contact
So how can I replicate the pattern above, but using a supplied callback fn to accomodate recaptcha? Should I be using GET instead of POST? Do I need any htmx attrs on form, if I am trying to submit it from js? It seems like callback is in effect, because form doesn’t submit without it. Put I wasn’t able to see any logging statements to verify (because of page reload?) Maybe i could try the other non-htmx pattern. I’m hoping to not have to re-do this all again without htmx. I thought htmx was supposed to help with this kind of stuff? Here is the callback: https://github.com/chromalchemy/groundedsol/blob/317195973cee22fc952b9a5a811f02f9db23d50e/src/gs/pages/contact.clj#L378C1-L390C20 Here is the form attrs https://github.com/chromalchemy/groundedsol/blob/317195973cee22fc952b9a5a811f02f9db23d50e/src/gs/pages/contact.clj#L461C7-L468C11 Submit btn: https://github.com/chromalchemy/groundedsol/blob/317195973cee22fc952b9a5a811f02f9db23d50e/src/gs/pages/contact.clj#L500C9-L508C17 Route: https://github.com/chromalchemy/groundedsol/blob/317195973cee22fc952b9a5a811f02f9db23d50e/src/gs/groundedsol/home.clj#L130C4-L132C47

chromalchemy21:01:01

Maybe there is minimum viable example of a event/callback that updates an form with htmx semantics somewhare, or I will try to build one again? Unless there’s some other complexity about re-captcha implementation I’m missing.

chromalchemy21:01:17

Rant: In a world of jangly, disjointed, repetative, brittle, inscrutable, boilerplate html/css/js, Biff’s reserved simplicity naturally leaves open many sharp edges, Despite htmx’s welcome affordances, I don’t yet have a great working model of http stateful flow yet, and the whole api surface area is feeling spread out and obtuse (not Biff’s fault). Of course validated captcha’d forms are probably the most complex thing, and I haven’t done all the tutorials from the ground up.. I’m just disappointed that I’m losing days to something I never learned to do properly in the PHP days. That’s what I get for trying to lean too hard on frameworks… That being said, the server side integration and seeing printouts in the terminal is cool, and clarifying. Need to learn to embrace the ctx and middleware patterns and practice more routing.

chromalchemy23:01:38

Should the js callback just submit the form? If it calls htmx.ajax('POST'....) Does that imply “submitting” the form? Or does it really need to to both, if it going to invoke htmx helpers?

chromalchemy01:01:09

This js for captcha callback worked. (sort of)

function submitContact(){
    const event = new Event('verified');
    const elem = document.querySelector("#contact-form");
    elem.dispatchEvent(event);
}
Create event verified , then listen for with with hx-trigger and the normal :hx- attrs. Works for first reload, updating form in place (using hx-swap. ) But then on subsequent reloads (for invalid form display) it errors out. Is this even an ok thing with captchas? Mulitple request/reload for server side validation? Like if form reloads, does it lose it’s token? If not, what is a best practice for captcha-protected (server) validated form? Necessarily reload whole page?

chromalchemy01:01:14

Alternatively any feedback on how/where to use htmx.ajax() fn in this “submit form” callback context?

Jacob O'Bryant03:01:42

reading through your messages now, responding as I go your htmx Ajax code looks correct to me. the error looks odd though--says you're getting a 405 error on the /contact endpoint, not /send-contact. however the htmx error has the correct /send-contact endpoint. not sure what's going on there.

Jacob O'Bryant03:01:20

POST is what you want, not GET

Jacob O'Bryant03:01:10

do you get a logging statement from the callback if you don't submit a request at all?

Jacob O'Bryant03:01:35

just looked at the code you linked. nothing jumps out at me; it looks fine.

Jacob O'Bryant03:01:36

I have found integrating with recaptcha to be quite fiddly at times. maybe tomorrow morning I can try to take the biff example app and do the recaptcha integration with htmx to see if I run into similar issues (if not, at least there'll be a working example then)

Jacob O'Bryant03:01:44

you shouldn't need to call .submit() if you're already calling htmx.ajax. though I haven't ever used htmx.ajax myself I don't think--would be worth me giving it a try

Jacob O'Bryant03:01:56

re: will reloading the form with htmx break recaptcha--good point. I don't know. need to think about that/experiment will try to finish reading your other messages tonight

Jacob O'Bryant04:01:39

if it's convenient to just reload the page, that will probably be easiest. honestly if you're OK using full page reloads in general for this form, it sounds like that would be a much easier approach. clearly there's some jankiness around getting htmx and recaptcha to play nicely. though again I'd be happy to try to get a simple htmx-recaptcha example working myself--it should only take a few minutes to try at least, and if I also can't get it to work then maybe we just admit defeat haha

Jacob O'Bryant04:01:51

I'm thinking again about "biff from scratch", an idea I had to write a series of guides showing how to do clojure web dev my composing the pieces yourself. something like that maybe would help with getting the fundamentals of http etc

Jacob O'Bryant08:01:15

Here's a working example: https://github.com/jacobobryant/biff/compare/master...htmx-recaptcha-example you can start a new biff project and apply those changes to home.clj if you want to run it. Notes: • I started out looking at the https://htmx.org/api/#ajax and noticed that there's an optional source parameter. I think that's why htmx.ajax wasn't working for you: if you do e.g. htmx.ajax('POST', '/foo', '#my-form'), that tells htmx that you want #my-form to be the target, but it doesn't make htmx include form parameters from #my-form in the request. You'd need to pass in an options map as the third parameter to htmx.ajax and set both source and target. • I was in the process of doing that, but then I came across this tidbit in the https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event: "The submit event fires when: [...] a script calls the form.requestSubmit() method. However, the event is not sent to the form when a script calls the form.submit() method directly." In other words, since htmx listens to the submit event on forms by default, form.requestMethod() will tell htmx to do its thing, but form.submit() will bypass htmx and submit the form the usual way. So in the example code, I added :hx-post and :hx-swap attributes to the form and then changed the recaptcha callback to do a requestSubmit() instead of a submit(). Then it worked. • The final bit: you were correct that the recaptcha form doesn't work if you re-render the form with htmx after a validation error without doing a full page load. When the recaptcha script loads, it looks for the first element with the g-recaptcha class and "renders"/binds itself to that element. So if you subsequently re-render that form, you have to tell recaptcha explicitly to re-initialize itself as well. I used hyperscript to call the grecaptcha-render() method when the form gets re-rendered after a validation failure (see the https://developers.google.com/recaptcha/docs/invisible). • I made it so if you type as the email address it'll be treated as a validation error. I was able to submit the form with as many times as I wanted, and then once I submitted it with another address, it showed the success message. Hope that helps!

chromalchemy14:01:39

Wow, incredible 🔥:star-struck: Great engineering. I was bouncing around but not so thorough! I was able to eventually try the html.ajax() call again and found the source parameter too. And seemed to be getting the first reload ok, but then ran into that captcha error. Thank you so much for really investigating the captcha dynamic! I slept on it and was resolved to rebuild with full page reloading, after looking closer at the example code and seeing more nuance in the handlers, and zero htmx there. I should have followed those more strictly, and was I guess tantilized and confused by the htmx options overlapping. I will practice to understand the classic request/routing/reload better. But it feels encouraging to know that htmx is working and available in this context. The captcha has been a core requirement of this simple site I’m rebuilding for family, which I refactored from a php site because I didn’t want to try to grok the server-side dynamics there. It’s been a big sticking point for me, and I’m thrilled to get proper implementation in place. Will work through you examples and report back. Thanks again for taking to time to look into it (again)!! 🙌:skin-tone-2:.

Jacob O'Bryant05:01:49

awesome, glad it's working! good luck with the rest of the project too! did you fix the recaptcha "site key not valid" problem? I'm guessing you just need to go to the recaptcha admin settings and add the domain to the whitelist.

chromalchemy00:01:37

Nothing changed. Not sure what was causing that, but rebooting everything fixed it.

👍 1