Open kucharzyk opened 2 years ago
Just checking whether @FroMage has not already done something along these ends for Renarde.
/cc @FroMage, @stuartwdouglas
This is a very interesting feature IMO.
Just checking whether @FroMage has not already done something along these ends for Renarde.
@FroMage, any input here?
I am also wondering whether it makes sense to do something Htmx specific, or provide the ability for user code to decide which Resource method should be executed...
I'm personally leaning towards the former
@geoand Quarkus shouldn't have anything specific to htmx. It should be generic feature. End user will decide how to use it
I don't agree, for a couple reasons:
FYI this is how it was solved in the example we are creating for Quinoa, I guess the same would be working: https://github.com/TeHMoroS/quinoa-request-control-discussion/blob/main/src/main/java/dev/dnadesigned/quinoa/TemplateGlobalVariables.java
and the template: https://github.com/TeHMoroS/quinoa-request-control-discussion/blob/main/src/main/resources/templates/common/base.html
And here is an ongoing discussion about Quarkus with Htmx (using NodeJS and Quinoa and Renarde) https://github.com/quarkiverse/quarkus-quinoa/discussions/113
Hi,
So, this is very interesting. Lemme learn more about htmx, but meanwhile I can already say you could simplify your code:
@GET
@Path("/form")
public TemplateInstance renderForm() {
return Templates.fullPageWithForm();
}
@GET
@Path("/form")
@HtmxPartial
public TemplateInstance renderFormPartial() {
return Templates.form();
}
Now, lemme first go read up on htmx, but one example of Turbo I've seen from Rails wasn't exactly what you're showing here with full page containing element X versus partial render of X. The example I'd seen was with a full page containing an order, which had comments, and the comments where rendered as partials (which I understand to be tags in the context of Qute), so you could render the whole page, or offload to the comment partials. Let's spit out some pseudo-code here:
public class Order extends PanacheEntity {
@OneToMany(mappedBy = "order")
public List<Comment> comments;
}
public class Comment extends PanacheEntity {
@ManyToOne
public Order order;
public String text;
}
public class Orders extends Controller {
public static class Templates {
// full page
public static native TemplateInstance order(Order order);
// partial
public static native TemplateInstance comments(Order order);
}
public TemplateInstance order(@RestPath long id) {
return Templates.order(Order.findById(id));
}
public TemplateInstance comments(@RestPath long id) {
return Templates.comments(Order.findById(id));
}
}
In templates/Orders/order.html
:
{#extend main.html}
{#set title "Order "+order.id}
{#comments order/}
{/extend}
In templates/tags/comments.html
:
{@model.Order order/}
<ul>
{#for comment in comments}
<li>{comment.text}</li>
{/for}
</ul>
But this doesn't need any special support on the RR side, aside from perhaps allowing type-safe template tags.
FYI this is how it was solved in the example we are creating for Quinoa, I guess the same would be working: https://github.com/TeHMoroS/quinoa-request-control-discussion/blob/main/src/main/java/dev/dnadesigned/quinoa/TemplateGlobalVariables.java
and the template: https://github.com/TeHMoroS/quinoa-request-control-discussion/blob/main/src/main/resources/templates/common/base.html
This is clever.
Side-note, this is how I used global vars too (via CDI), but recent releases added support for them explicitely: https://quarkus.io/guides/qute-reference#global_variables
But this doesn't need any special support on the RR side
How is
public TemplateInstance order(@RestPath long id) {
return Templates.order(Order.findById(id));
}
public TemplateInstance comments(@RestPath long id) {
return Templates.comments(Order.findById(id));
}
How are these two dissambiguated? Are they different paths?
For htmlx
, the dissambiguation happens because there is a special HTTP header.
They're not, but I don't think they need to be. Oh, this is renarde, so they have @Path("order")
and @Path("comments")
added.
I need to look more into this, because in the demo I saw, it didn't seem to work quite like that.
Hm... in the few code samples I saw things were controlled via some special HTTP headers.
@FroMage do you look into htmx perhaps? It would be great to figure this out so we can move forward with supporting it
Just to add a tiny note in case we do decide to go down the route of providing some kind of API that would allow for selection, that MediaTypeMapper
essentially does what we are discussing by matching the media types mentioned in the request to the media types declared by the method.
Of course if we do decide to go down this route, we would not want to expose the internal types and APIs, but some more restricted and easier to use form.
OK, so I finally read up on Turbo and was a bit perplexed by their docs, which I find confusing, as it implied you didn't have to modify your endpoint at all: you could just return the entire page with the normal endpoint and as long as you had a <turbo-frame id="message_1">
that matched in there, it would drop the rest of the page and swap the matching frame with the new content. Which means that the endpoint could be the same, and returning a full page or not was a matter of rendering performance. The client would behave the same.
But the endpoint would be the same, and so its cost would be the same as well, say if it fetched stuff from the DB.
Micronaut has a similar approach: https://micronaut-projects.github.io/micronaut-views/latest/guide/#turbo where the endpoint is shared:
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
Map<String, Object> index() {
return Collections.singletonMap("message",
new Message(1L, "My message title", "My message content"));
}
But the views differ, because here is the full view, identified by @View("edit")
which I guess matches if there's no special header:
<!DOCTYPE html>
<html>
<head>
<title>Edit</title>
</head>
<body>
<h1>Editing message</h1>
<turbo-frame id="message_$message.getId()">
#parse("views/form.vm")
</turbo-frame>
</body>
</html>
And here's the partial view, used by the full view as an include, specified by the @TurboFrameView("form")
if the header matches, I guess:
<form action="/messages/$message.getId()">
<input name="name" type="text" value="$message.getName()">
<textarea name="content">$message.getContent()</textarea>
<input type="submit">
</form>
So in this case it's obvious we're not rendering the same template, but it's still the same endpoint. This doesn't really match what I had understood in the case where we're rendering several messages from a single page. Also I've no idea how they can surround the partial template with the required <turbo-frame id="message_1">
element with that setup.
I also realise that htmx and Turbo appear to be different ways to achieve something comparable, but with different tech.
From the Request/Response header docs of Htmx it appears that at a minimum we should be providing an API to read/write Htmx headers.
So, yeah, Htmx and Turbo appear to be completely different technologies that happen to have a similar backend decomposition solution. We should probably do two separate extensions but keep the solution/API similar.
Thanks for the insights @FroMage
Wouldn’t it be better to create only support for conditional routes in resteasy. With such flexible foundation everybody could create solution for him with few lines of code.
After that we could think about supporting specific frameworks.
We'll see. If we do do that , I mentioned above what needs to be done.
This annotation you're describing is very much a generalisation of content-type matching. If Htmx had a Accept: text/htmx, text/html
header we would be able to write:
@GET
@Path("/form")
@Produces(MediaType.TEXT_HTML)
public Uni<String> renderForm(@Context HttpHeaders headers) {
return Templates.fullPageWithForm().createUni();
}
@GET
@Path("/form")
@Produces("text/htmx")
public Uni<String> renderFormPartial(@Context HttpHeaders headers) {
return Templates.form().createUni();
}
And call this a day. In fact, we have request negociation/matching for URI,HTTP method,Accept and Content-Type, but that's pretty much it, they're all hard-coded. There's no user code that could help select methods, ATM.
Adding an API for that for any user code would mean having conditional routing for rules that are not hard-coded. It would complexify our routing/matching algo (unless we can rewrite it using this new abstraction) but it would also be less efficient than static routing which we try to promote.
Now, I'd like to see a real use-case for having separate endpoints, and not simply return the same full document as advertised by Turbo (they strip the outer elements) or do the filtering in the views like Andy showed.
I'm thinking about real different logic.
I'd like to see a real use-case for having separate endpoints
+1
The filtering in the template makes quite some sense:
I am currently evaluating a possible Quarkiverse extension to make it all integrated and easy to use: It would allow built-in assets processing (css purging, svg image optim, sass support, ... ) through Quinoa, and avoid all the logic related to making htmx work. Comaptible with RESTEasy Reactive and Renarde.
@ia3andy @FroMage ww should probably get together and figure something like this out. I feel like there is a good opportunity here
maybe i'm missing something but isn't the use case of having a endpoint that can either serve a json model response vs htmx client ui rendered response what gives quite different logic and thus having two different methods is nice ?
Yeah, but ideally you want that to be dead simple
Now, I'd like to see a real use-case for having separate endpoints, and not simply return the same full document as advertised by Turbo (they strip the outer elements) or do the filtering in the views like Andy showed.
Isn't that "use case" obvious? Don't run code you don't need. If you return the full view, you most likely run code you don't need. If you filter the output in the view, you either run code you don't need (same as previous option), or you let the view assume the role of the controller.
(If that feels like "premature optimization" to some, I'm gonna claim this is not optimization at all. Not running code you don't need, when you know you don't need to run the code, that's just common sense.)
Of course the argument above makes no sense when rendering the desired part of the view requires running 99% of the code you'd have to run for the full view. In my experience, that's seldom the case, but I admit my experience is mostly from rather dynamic websites with multiple independent parts contributing to the resulting page.
Description
Hello,
I would like to create multiple endpoints with the same url. Depending on request headers I would like to decide which method should be used. Currently I have one endpoint with conditional return statement but it is not ideal solution for me.
It would be nice to create multiple endpoints with same url i and annotate them with custom annotations. I was looking in docs and in the source code but I can't find interceptor which I could use. I think it is not yet possible.
My use case is related to Qute and Htmx library:
Depending on header presence I need to render whole page or only partial response.
Instead of this code I would like to create two separate endpoints and annotate them.
This is approach can be implemented for example in Spring (https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf). It would like to recreate something similar in Resteasy but I need intercept endpoint choose decision.
I think there will be much more use cases for it. For example we could create API annotated with
@APIv2
without touching old endpoints and serve new API depending on request headers.@geoand @Ladicek @maxandersen
Implementation ideas
No response