Fork me on GitHub
#biff
<
2023-02-24
>
Paul Hempel14:02:33

Hey everyone. I started using biff and was wondering, if - or better where - it is possible to add ring.middleware.refresh/wrap-refresh to work properly. Adding it to the handler to

(def handler (-> (biff/reitit-handler {:routes routes})
                 biff/wrap-base-defaults))
does nothing, and yields an error when added to routes (I don’t think that it works that way judging from the docs)
(def features
  {:routes ["/app" {:middleware [refresh/wrap-refresh
                                 mid/wrap-signed-in]}
            ["" {:get app}]]})
https://github.com/weavejester/ring-refresh Does anyone have experience with it? 🙂

Jacob O'Bryant15:02:21

I haven't tried adding that middleware myself, however biff already does a refresh whenever you save a file. Has that been working for you?

Paul Hempel15:02:34

Yes. The Backend does update just fine. The goal is that the browser updates itself automatically as well, when the backend refreshes. Which is also the promise of wrap-refresh – not to be confused with wrap-reload (which grants the backend NS reloading functionality) ^^

Jacob O'Bryant15:02:12

oh gotcha, I was indeed confusing it with wrap-reload :)

Paul Hempel15:02:30

Yeah, me too at beginning 😄

Paul Hempel15:02:45

(use 'ring.middleware.refresh)

(def app
  (wrap-refresh your-handler))
The wrap-refresh docs basically say to do this. But this is where the levels of Biffs abstractions go over my head

Jacob O'Bryant15:02:57

(doesn't help that tools.deps uses the term refresh IIRC!) There's an issue for this on the biff repo--I'm pretty sure I did get something working, using a js lib that someone suggested. The ideal solution would probably involve opening a web socket connection with htmx and then having the server send a refresh command whenever it detects a file change. shouldn't actually be that hard to add, I just haven't gotten around to it yet. I'm unaware of how wrap-refresh is implemented and if that would or wouldn't work...

Jacob O'Bryant15:02:04

(the way you've added it to the middleware in Biff looks correct to me)

Paul Hempel15:02:02

https://github.com/weavejester/ring-refresh/blob/master/resources/ring/js/refresh.js It’s actually just this JS function slapped at the end of the body. So yes, should work like any other middle ware

Jacob O'Bryant16:02:06

hm yeah, looks like what should work fine. if you add the middleware and refresh the page manually, does that snippet show up in the page source? if so, do you see it making any requests (either in the network tab or in the terminal)? here's an example that does a similar thing using live.js, albeit with a lot more code: https://github.com/jacobobryant/biff/commit/36cfe9b291185c9300239770059fd0e72a2bea05

Paul Hempel16:02:27

I just checked it in another project using the same ring+rum setup without Biff: It does not work if I add it to the routes, only if I add it to the ‘wrap-base’ :

; this is the default luminus code
(defn app []
  (middleware/wrap-base #'app-routes))
...
(defn wrap-base [handler]
  (-> ((:middleware defaults) handler)
      refresh/wrap-refresh

Paul Hempel16:02:19

It did show up in the source code, even when I added it to the routes table, but it didn’t do anything.

(defn home-routes
  []
  [""
   {:middleware [middleware/wrap-csrf
                 ...
                 refresh/wrap-refresh]}
   ["/" {:get  page}]

Paul Hempel16:02:13

Well for today no more Clojure for me. I’ll test it over the weekend and hopefully can find a solution. So far thanks for the help @U7YNGKDHA!

Jacob O'Bryant16:02:59

👍👍 good luck! I'll peek at the wrap-refresh source at least and see if anything comes to mind

Paul Hempel08:02:27

I “fixed” it by adding the https://github.com/weavejester/ring-refresh/blob/master/resources/ring/js/refresh.js file to the project resources and linked it manually. The suggestion to check on if the script was actually showing in source code on the page was spot on. Though I’m wondering why this happens.

(def handler (-> (biff/reitit-handler {:routes routes})
                 biff/wrap-base-defaults
                 refresh/wrap-refresh))
Only the handler needed to be extended with the refresh middleware.

Jacob O'Bryant16:02:29

Ah, glad it's working. I just looked at the source and I think this line is why the script doesn't get injected: https://github.com/weavejester/ring-refresh/blob/master/src/ring/middleware/refresh.clj#L16 wrap-refresh checks the "Content-Type" header and only injects the JS script if it includes text/html, but https://github.com/jacobobryant/biff/blob/master/src/com/biffweb/impl/rum.clj#L10 the "content-type" header (lower case). I'm not sure if ring handlers are supposed to set the "Content-Type" header or if the middleware should've been checking in a case-insensitive way. I think it's the latter, but 🤷 . In any case, linking to the script manually is a good solution anyway.

brianwitte18:02:15

Just starting exploring biff (looks awesome! 😃) and I am very new to htmx or really anything frontend related. I created a new project and have added the functions below to feat/app.clj. I can successfully add a new "blub" and rerender the page and see it like I want, but my implementation feels quite wrong, haha. if you look through the create-blub function, I am submitting a Create tx to the database, calling (biff/render (blub-form req)) then re-rendering the whole page after redirecting via a 303 map. The only thing I have added to the scaffolded (defn app ...) function is an h1 tag and another (first) call to (blub-form req) . I was just hoping that I could get some feedback on this snippet in thread below and be pointed towards the most "idiomatic" way of doing this. To reiterate, I am just trying to create a new thing in the database and have the page render that thing upon submission (clicking button). My current code works, but seems wrong/inefficient.

brianwitte18:02:47

;; added this to (def schema ...) in schema.clj
;;
;;   :blub/id :uuid
;;   :blub/name :string
;;   :blub/stage :string
;;   :blub/created-at inst?
;;   :blub [:map {:closed true}
;;          [:xt/id :blub/id]
;;          :blub/created-at
;;          [:blub/name {:optional false}]
;;          [:blub/stage {:optional false}]]

;; below is in feat/app.clj

(defn blub [{:blub/keys [name]}]
  [:.mt-3 {:_ "init send newBlub to #blub-header"}
   [:.text-gray-600 "blub!: "]
   [:div name]])

(defn blub-form [{:keys [biff/db]}]
  (let [blubs (q db
                 '{:find (pull blub [*])
                   :where [[blub :blub/name]]})]
    (biff/form
     {:hx-post "/app/create-blub"
      :hx-swap "outerHTML"
      :hx-target "closest div"}
     [:label.block {:for "name"} "Add a blub: "]
     [:.h-1]
     [:.flex
      [:input.w-full#name {:type "text" :name "name"}]
      [:.w-3]
      [:button.btn {:type "submit"} "Create"]]
     [:.h-1]
     [:.text-sm.text-gray-600
      "This demonstrates updating a value with HTMX."]
     [:.h-6]
     [:div#blub-header
      {:_ "on newBlub put 'Blubs created:' into me"}
;;      (if (empty? blubs)
;;        "No blubs yet."
;;        "Messages sent in the past 10 minutes:")
      ]
     [:div#blubs
      (map blub (sort-by :blub/name #(compare %1 %2) blubs))])))

(defn create-blub [{:keys [session params] :as req}]
  (biff/submit-tx req
    [{:db/op :create
      :db/doc-type :blub
      :xt/id (random-uuid)
      :blub/name (:name params)
      :blub/stage "Setup"
      :blub/created-at (java.time.Instant/now)}])
  (biff/render (blub-form req))
  {:status 303
   :headers {"Location" (str "/app")}})

(defn app [{:keys [session biff/db] :as req}]
  (let [{:user/keys [email foo bar]} (xt/entity db (:uid session))]
    (ui/page
     {}
     nil
     [:div "Signed in as " email ". "
      (biff/form
       {:action "/auth/signout"
        :class "inline"}
       [:button.text-blue-500.hover:text-blue-800 {:type "submit"}
        "Sign out"])
      "."]
     [:.h-6]
     (blub-form req)
     [:.h-6]
     (chat req))))

Jacob O'Bryant23:02:28

Hey! Just looked over this. The overall approach is fine. Here are a few things that came to mind: 1. Instead of :blub/stage :string, you probably want to use an enum, e.g. :blub/stage [:enum :setup :in-progress :done]. 2. There's no need to specify {:optional false}; keys are required by default. For any optional keys, you'll want to specify {:optional true}. 3. When you make an htmx request (`:hx-post "/app/create-blub" ...`), the backend handler should generally return html immediately instead of redirecting to another endpoint. you're mixing an htmx request with a "regular" backend handler. I'm actually not sure exactly what happens if you make an htmx request and then the handler returns a redirect... since it's working, I assume htmx just redirects the the whole page, as if it were a regular form POST 🤷 . You have a couple options: The classic way You can have the handler continue to redirect. In this case, there's no need to use an htmx request--you can just do (biff/form {:action "/app/create-blub"} ...). (`biff/form` sets the method to "POST" by default). In create-blub, you can delete the (biff/render (blub-form req)) line--it's not doing anything right now anyway, since the {:status 303 ...} value is the function's return value. The return value of biff/render is just getting discarded. The htmx way Using an htmx request is handy for when you want to avoid reloading the whole page (e.g. so you don't lose your scroll position or other state on the page, or just to make things a little snappier). If you do an htmx request here, change create-blub so it returns (blub-form req). (i.e. delete the {:status 303 ...} value--also no need to call biff/render because biff includes some middleware that does that for you if the handler returns a rum data structure). Then remove the :hx-target, i.e. just do (biff/form {:hx-post "/app/create-blub" :hx-swap "outerHTML"} ...). That way, the blub-form form will replace itself, instead of replacing the parent div. Either way, there's also no need to use the newBlub event. Instead you can do a (empty? blubs) check, similar to what's in the commented code: [:div (if (empty? blubs) "No blubs yet." "Blubs created:")]. (Looking back at Biff's example code, even the newMessage event isn't really necessary--I'll probably rewrite that to use an out-of-band swap instead.)

💯 2
brianwitte00:02:29

thank you very much getting back with such a comprehensive answer and thank you for this project! I am gonna keep hacking away...

Jacob O'Bryant01:02:33

you're welcome! definitely post any more questions you come up with.