whamtet / simpleui

JS Free Single Page Applications
https://simpleui.io
Eclipse Public License 2.0
251 stars 15 forks source link

Term `middleware` is confusing; other thoughts #27

Closed telekid closed 1 year ago

telekid commented 1 year ago

First, thanks for getting back to me regarding my previous question, and for making this library in the first place. I've enjoyed exploring it so far!

I'm following up on a message I dropped in the clojurians #ctmx channel.

Generally, when I see the word middleware – especially in a ring context – I think of a function like this:

(defn wrap-hx-redirect [handler]
  (fn [req]
    (let [req (handler req)]
      (update req :headers assoc "hx-redirect" "/"))))

That is, I expect a function that takes a handler and returns a new handler that typically invokes the passed handler with the req argument.

ctmx's ^{:req middleware} metadata is confusing, as it doesn't behave as we typically expect middleware to behave; middleware is rather expected to be something akin to a request transformer, i.e. a simpler function of type req -> req.

A few thoughts.

To start, I'm wondering if there a reason that you've created ctmx's middleware such that it is capable of transforming the req map passed into a component, but cannot transform the response map returned by it? My hunch is that this restriction is intentional, though it isn't immediately clear to me why that might be. (Perhaps you feel the semantics of the interaction between ctmx's middleware and ring's middleware stack to be a bit tricky to define, particularly in the case of nested components? Just guessing here.)

I ask because component middleware seems like a fairly elegant way to separate view and controller concerns, but this approach is fairly limited if one cannot update headers in the response object, for example.

If this restriction is intentional, then I might consider changing the name from middleware to something else to reduce confusion.

But I should also ask: am I barking up the wrong tree here? Do you recommend a different strategy for making modifications to the response object that extend beyond the :body key? Imagine that one is making a login component, for example, where login success should add :user-identity info to a :session map on the response object. How would you go about that currently?

whamtet commented 1 year ago

Excellent feedback @telekid,

I think you're right that we should use a better name than middleware for the reason you state. The key idea is that we might want to transform the request before we bind request parameters to the function argument. We only want to do that when the function is being updated, not on the initial render. That's why its different from regular ring middleware. For the case where you want to transform the response object that should be handled by regular ring middleware.

For the case when you want to update e.g. headers simply use the req middleware (name soon to be changed) as it receives the entire request object. I think prebind might be a better name than middleware, do you agree?

telekid commented 1 year ago

The key idea is that we might want to transform the request before we bind request parameters to the function argument

Makes sense.

We only want to do that when the function is being updated, not on the initial render.

Makes sense, though (I think?) I can imagine a different implementation that conditionally applies a traditional ring middleware within the standard ring middleware chain (but included via your current req metadata strategy) when the component is serving as an endpoint root, but not when it is participating as a child component of a different render.

For the case when you want to update e.g. headers simply use the req middleware (name soon to be changed) as it receives the entire request object

In my case I'm interested in updating response headers, not request headers, so this won't quite do. I am still having trouble imaging how I might modify response headers as part of the same request that is returning an HTML fragment as part of a ctmx component re-render. Where would I put that traditional middleware? Should I modify the routes returned by ctmx's make-routes function, manually appending middleware to the route that corresponds to the component in question? My initial reaction is that doing so feels a bit cumbersome.

whamtet commented 1 year ago

I would suggest you have two options. Firstly you can always return a regular ring response map from component update. By default when you return html represented as hiccup it is rendered and put into a response map. If the component returns a map ctmx assumes it's already a valid ring response.

You can also use regular ring middleware and check for the HX-Request=true header. Details. This is not set on initial render. The limitation is that it doesn't know exactly which component it is going to.

I could modify the existing middleware option to receive both request and response but I wonder if its easier to just return a map directly from the component. What do you think?

telekid commented 1 year ago

Apologies for the slow response!

I could modify the existing middleware option to receive both request and response but I wonder if its easier to just return a map directly from the component. What do you think?

This makes sense. Not sure why this hadn't occurred to me the first time around. I'll give this a shot next time I'm working in that area of my codebase.

Thanks for your help!

whamtet commented 1 year ago

@telekid here's a PR to address the issue https://github.com/whamtet/ctmx/pull/28/files