htmx-spring-boot
htmx-spring-boot copied to clipboard
Add HxPush annotation
We have a working HxPush annotation using an Interceptor, should I create a PR?
@Repeatable(HxPushs.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@HxRequest
public @interface HxPush {
String url() default "";
String urlPrefix() default "";
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface HxPushs {
HxPush[] value();
}
}
public class HtmxPushUrlInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull Object handler) {
if (handler instanceof HandlerMethod handlerMethod) {
var hxPushUrls =
AnnotatedElementUtils.findMergedRepeatableAnnotations(
handlerMethod.getMethod(), HxPush.class, HxPush.HxPushs.class);
if (CollectionUtils.isNotEmpty(hxPushUrls)) {
var refererPath =
String.valueOf(request.getHeader("referer"))
.replace(String.valueOf(request.getHeader("origin")), "");
var matchingPrefix =
hxPushUrls.stream()
.filter(
url ->
Strings.isNotEmpty(url.urlPrefix())
&& refererPath.startsWith(url.urlPrefix()))
.findFirst();
var withoutPrefix =
hxPushUrls.stream().filter(url -> Strings.isEmpty(url.urlPrefix()))
.findFirst();
if (matchingPrefix.isPresent()) {
addHeader(request, response, matchingPrefix.get());
return true;
}
withoutPrefix.ifPresent((prefix) -> addHeader(request, response, prefix));
return true;
}
}
return true;
}
private void addHeader(
@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
HxPush hxPushUrl) {
if (Strings.isBlank(hxPushUrl.url())) {
if (request.getQueryString() != null) {
response.addHeader("HX-Push-Url",
request.getServletPath() + "?" + request.getQueryString());
return;
}
response.addHeader("HX-Push-Url", request.getServletPath());
} else {
response.addHeader("HX-Push-Url", hxPushUrl.url());
}
}
}
Can you give some examples of where you would use this?
Any time I make a HX Request with dynamic properties and I want to push the URL into the browser history:
So it is an annotation alternative to using the HtmxResponse
return type if I understand it correctly. You can already do this now:
@HxRequest
@GetMapping(path=TasksWebPath.OVERVIEW_SCOPE)
public HtmxResponse taskOverview(@PathVariable("scope") String scope) {
ViewContext viewContext = taskOverview.renderTaskOverview(TaskScope.validate(scope));
return HtmxResponse.builder.addTemplate(viewContext).pushUrl("/the/url/here").build();
}
Not as convenient, and we require the URL in the pushUrl
method. I think I like this :-)
I also like it. It will also need to support setting a custom url:
@HxRequest
@HxPush //Implies the value of the current request mapping
@GetMapping(path=TasksWebPath.OVERVIEW_SCOPE)
public HtmxResponse taskOverview(@PathVariable("scope") String scope) {
ViewContext viewContext = taskOverview.renderTaskOverview(TaskScope.validate(scope));
return HtmxResponse.builder.addTemplate(viewContext).build();
}
and
@HxRequest
@HxPush("/alternate/path")
@GetMapping(path=TasksWebPath.OVERVIEW_SCOPE)
public HtmxResponse taskOverview(@PathVariable("scope") String scope) {
ViewContext viewContext = taskOverview.renderTaskOverview(TaskScope.validate(scope));
return HtmxResponse.builder.addTemplate(viewContext).build();
}
Yeah most of my improvement ideas do not consider the HtmxResponse builder as I use ViewContexts instead.
Just a reminder: I have already done the work for this and all other missing response header annotations in PR https://github.com/wimdeblauwe/htmx-spring-boot/pull/67 (Commit for @HxPush
https://github.com/wimdeblauwe/htmx-spring-boot/pull/67/commits/c2020f56b13d4120bba2ece066e473e28fd9754c).
If you want to push the current request URL to the browser history just set pushUrl
to true
in HtmxResonse.builder()
. BTW the docs for HX-Push-Url are not clear enough about that, but all possible values of hx-push-url also apply to the header.
@HxRequest
@GetMapping(path=TasksWebPath.OVERVIEW_SCOPE)
public HtmxResponse taskOverview(@PathVariable("scope") String scope) {
return HtmxResponse.builder()
.view(taskOverview.renderTaskOverview(TaskScope.validate(scope));)
.pushUrl("true") // or .pushUrl("/alternative/URL")
.build();
}
BTW What I could imagine to simplify the use of the builder a bit would be a convenience method e.g. pushUrl()
without a parameter. We already had that for pushUrl("false")
via preventHistoryUpdate()
Right, thanks for reminding me about that @xhaggi. I don't have much time to work on this, but I would be happy to review a PR if somebody wants to take this up.
@tschuehly could you please give us some more insight into your code? Why do you need a prefix and why do you add the request.getServletPath()
in case @HxPush
is blank?
I would recommend not moving too much htmx-specific code to the server side. If you want to push the current URL that htmx has requested to the browser history, simply add the attribute hx-push-url="true" to the HTML element in your template code.
We needed a prefix when we had multiple @hxpush
annotations but I would probably remove it.
For us @Hxpush
most often has no path as the paths are dynamic. The templates are created on the server so I see no difference between doing it on the template or the Endpoint.
I prefer doing it on the Endpoints as I don't know at component creation time if I need to push the URL or not.
@wimdeblauwe @xhaggi I've ported xhaggis work to the current main and opened a pull request: https://github.com/wimdeblauwe/htmx-spring-boot/pull/117
#118 has been merged now and @HxPushUrl
is part of it.