Tom Bombadil
Tom Bombadil

Reputation: 63

Spring TemplateEngine process error on th:field

I am developing a web application in Java using spring.

This application includes Ajax calls in javascript which requests html code that is then inserted into the html document.

In order to process a thymeleaf template into a String i'm using TemplateEngine process(..) method.

I encountered an error when the thymeleaf template contains a form.

My sample code:

form.html:

<form th:object="${customer}" xmlns:th="http://www.w3.org/1999/xhtml">
    <label>Name</label>
    <input type="text" th:field="*{name}" />
</form>

AjaxController.java:

package project;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Controller
public class AjaxController {

    @Autowired
    private TemplateEngine templateEngine;
    private ObjectMapper objectMapper = new ObjectMapper();

    @ResponseBody
    @GetMapping(value="/form1")
    public String form1() throws JsonProcessingException {

        Customer customer = new Customer("Burger King");

        Context templateContext = new Context();
        templateContext.setVariable("customer", customer);

        AjaxResponse response = new AjaxResponse();
        response.html = templateEngine.process("form", templateContext);
        response.additionalData = "ab123";

        return objectMapper.writeValueAsString(response);
    }

    @GetMapping(value="/form2")
    public String form2(Model model) throws JsonProcessingException {

        Customer customer = new Customer("Burger King");

        model.addAttribute("customer", customer);

        return "form";
    }

    class Customer {
        private String name;

        public Customer(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    class AjaxResponse {
        public String html;
        public String additionalData;
    }
}

form1 is the one crashing, I'm trying to return the html code parsed by the thymeleaf template and also include additional data in this json response. It crashes on the line templateEngine.process("form", templateContext);

form1 works when replacing form.html with:

Customer name is: [[${customer.name}]]

Which leads me to conclude that it is the form tag and th:object which causes this to crash.

form2 works just as expected, but without any way to manipulate the thymeleaf return value. It proves that the thymeleaf template itself is valid.

The whole error output is a bit too massive to paste in here but:

org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/form.html]")

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Cannot process attribute '{th:field,data-th-field}': no associated BindStatus could be found for the intended form binding operations. This can be due to the lack of a proper management of the Spring RequestContext, which is usually done through the ThymeleafView or ThymeleafReactiveView (template: "form" - line 3, col 21)

My question is: Is this a bug in the spring framework? or if not then what am i doing wrong?


Update 1: Replacing th:field with th:value makes it work, seems that th:field inside a form when using TemplateEngine .process is what produces the error.

Update 2: Okay so after a lot of detective work i've figured out a sort of hack to make this work temporarily. The problem is that thymeleaf requires IThymeleafRequestContext to process a template with a form, When TemplateEngine .process runs then this will not be created. It is possible to inject this into your model like following:

@Autowired
ServletContext servletContext;

private String renderToString(HttpServletRequest request, HttpServletResponse response, String viewName, Map<String, Object> parameters) {
    Context templateContext = new Context();
    templateContext.setVariables(parameters);

    RequestContext requestContext = new RequestContext(request, response, servletContext, parameters);
    SpringWebMvcThymeleafRequestContext thymeleafRequestContext = new SpringWebMvcThymeleafRequestContext(requestContext, request);
            templateContext.setVariable("thymeleafRequestContext", thymeleafRequestContext);

    return templateEngine.process(viewName, templateContext);
}

and now you use this method like this:

@ResponseBody
@GetMapping(value="/form1")
public String form1(HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException {

    Customer customer = new Customer("Burger King");
    BindingAwareModelMap bindingMap = new BindingAwareModelMap();
    bindingMap.addAttribute("customer", customer);
    String html = renderToString(request, response, "form", bindingMap);

    AjaxResponse resp = new AjaxResponse();
    resp.html = html;
    resp.additionalData = "ab123";

    String json = objectMapper.writeValueAsString(resp);
    return json;
}

I will not put down this as an answer as i don't see any reason of this being intended to be used this way. I'm in communication with the spring people to get a real fix for this.

Upvotes: 5

Views: 4847

Answers (2)

Brian Clozel
Brian Clozel

Reputation: 59231

It seems you're trying to manually render an HTML template, outside of the web request context, return it serialized as an AJAX response - but still expect form binding to work. This is the key problem here.

Using th:field in a template means that you're expecting form binding from the HTTP request. In your code snippet, you're providing an empty, non-web context and still expect form binding to happen.

Since Thymeleaf can be used in various contexts (like rendering an email template before sending a newsletter, rendering a document in a batch application), we can't enforce a web context in all cases.

When rendering views the way Spring Framework expects things (by returning the view name as the return value of the controller handler), Spring will use and configure Thymeleaf accordingly.

Your answer is technically valid because it solves your problem, but it comes from the convoluted constraint of rendering a template and wrap that into a json String, and still expect HTTP binding.

Upvotes: 1

riddle_me_this
riddle_me_this

Reputation: 9155

Welcome to SO.

Remove xmlns:th="http://www.w3.org/1999/xhtml" from the form tag. This is not proper syntax. This would belong in the html tag.

You can find plenty of clear examples in the docs.

Upvotes: 1

Related Questions