Skip to content

Query params

Query paramsIn a request for /people?q=ada&sort=email,desc&size=50&page=2. Sending the form asks for /people?q=…&sort=email,desc&size=50: the order and the size stay, the page starts again at the first.
Thymeleaf source
<form action="/people" method="get">
<sl:label for="query-params-q">Search people</sl:label>
<sl:input type="search" id="query-params-q" name="q" th:value="${param.q}" placeholder="Name or email"/>
<sl:query-params except="q"/>
</form>
Rendered HTML
<form action="/people" method="get">
<label class="label" for="query-params-q">Search people</label>
<input class="input" type="search" id="query-params-q" name="q" value="ada" placeholder="Name or email">
<input type="hidden" name="sort" value="email,desc">
<input type="hidden" name="size" value="50">
</form>

A search above a table is a GET form: sending it asks for the same page with ?q=..., which the controller filters on. A browser sends only the form’s own fields, so on its own the form would forget the table’s order and page size. <sl:query-params> copies the current request’s other parameters into the form as hidden inputs:

<form th:action="@{/people}" method="get" role="search">
<sl:input type="search" name="q" th:value="${param.q}" aria-label="Search people"/>
<sl:query-params except="q"/>
</form>

In a request for /people?q=ada&sort=email,desc&size=50&page=2, that renders:

<form action="/people" method="get" role="search">
<input class="input" type="search" name="q" value="ada" aria-label="Search people">
<input type="hidden" name="sort" value="email,desc">
<input type="hidden" name="size" value="50">
</form>

and searching for “grace” asks for /people?q=grace&sort=email,desc&size=50.

  • except names the form’s own fields, separated by commas (except="q,status"). Without it, q would be sent twice: the field’s new value and the hidden copy of the old one.
  • The page number is always left out, so a new search starts on the first page. It is the page parameter your Pageable reads (spring.data.web.pageable.*, see parameter names); with qualifier="members" it is members_page, for the second table on a page.
  • A repeated parameter stays repeated, in its order: ?sort=name,desc&sort=email gives two sort inputs, as Spring Data’s Sort reads them.
  • Values are decoded once (?q=ada%20l%C3%A9 gives value="ada lé") and escaped like any other attribute value, so a parameter cannot add markup.

The controller needs nothing for it: the Pageable reads sort and size as it does for the table’s links.

@GetMapping("/people")
String people(@RequestParam(required = false) String q, @PageableDefault(sort = "name") Pageable pageable,
Model model) {
Page<Person> people = q == null || q.isBlank()
? personRepository.findAll(pageable)
: personRepository.findByNameContainingIgnoreCaseOrEmailContainingIgnoreCase(q, q, pageable);
model.addAttribute("people", people);
return "people";
}

The links of the table’s headers, its pagination and the rows-per-page menu keep q in the same way, as they are built from the request too.

Other attributes go to every hidden input. With form, the inputs belong to a form elsewhere on the page. The live search with htmx uses this to put them next to the table, so they are swapped with it and follow the order of the table they came with:

<form id="people-search" ...>...</form>
<div id="people-results">
<sl:query-params except="q" form="people-search"/>
<sl:table th:page="${people}">...</sl:table>
</div>
AttributeValuesDefaultDescription
exceptstring—The parameters not to copy, separated by commas: the fields the form has itself (q, q,status).
qualifierstring—The @Qualifier of the controller's Pageable, for a second table on the page: its page parameter (members_page) is the one left out.

Every prop can also be set from an expression: th:except="${...}". Any other attribute passes through to the rendered element, and class is appended.