0

I have an edit form for a Project object. Each project has a list of roles associated with them out of the complete list of all available roles. I need a checkbox list to select the roles. I implemented it like in this code sample I found thanks to StackOverflow, using a Formatter: https://github.com/jmiguelsamper/thymeleafexamples-selectmultiple

The issue: The form I created allows me to select the roles and save them successfully. But when I display an existing project object to edit it, the roles already associated with that project are not checked in the list. All the checkboxes are clear.

The code sample above was with a String id. I use a Long id. I think that's the reason for the issue, but I don't know how to solve it. Should I drop the Formatter approach entirely? Is there a way to make this work?

This is my code so far:

Project class:

@Entity
public class Project
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany
    private List<Role> rolesNeeded;

    public Project()
    {
        rolesNeeded = new ArrayList<>();
    }

    //getters and setters omitted for brevity
}

Role class:

@Entity
public class Role
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Column
    private String name;

    public Role() {}

    //getters and setters omitted for brevity
}

Controller:

@Controller
public class ProjectController
{
    @Autowired
    private ProjectService projectService;

    @Autowired
    private RoleService roleService;

    @RequestMapping(value = "/projects/save", method = RequestMethod.POST)
    public String addProject(@Valid Project project)
    {
        projectService.save(project);

        return "redirect:/";
    }

    @RequestMapping("/projects/{id}/edit")
    public String editForm(@PathVariable Long id, Model model)
    {
        Project project = projectService.findById(id);
        model.addAttribute("project", project);
        model.addAttribute("allRoles", roleService.findAll());

        return "project/form";
    }
}

The RoleFormatter:

@Component
public class RoleFormatter implements Formatter<Role>
{
    @Override
    public Role parse(String id, Locale locale) throws ParseException
    {
        Role role = new Role();
        role.setId(Long.parseLong(id));
        return role;
    }

    @Override
    public String print(Role role, Locale locale)
    {
        String id = role.getId() + "";
        return id;
    }
}

And finally the Thymeleaf form:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<section>
    <div class="container wrapper">
        <form th:action="@{/projects/save}" method="post" th:object="${project}">
            <input type="hidden" th:field="*{id}"/>
            <div>
                <label for="project_name"> Project Name:</label>
                <input type="text" id="project_name" th:field="*{name}"/>
            </div>
            <div>
                <label>Project Roles:</label>
                <ul class="checkbox-list">
                    <li th:each="role : ${allRoles}">
                        <input type="checkbox" th:id="${{role}}" th:value="${{role}}" th:field="*{rolesNeeded}" />
                        <span class="primary" th:text="${role.name}"></span>
                    </li>
                </ul>
            </div>
            <div class="actions">
                <button type="submit" value="Save" class="button">Save</button>
                <a th:href="@{/}" class="button button-secondary">Cancel</a>
            </div>
        </form>
    </div>
</section>
</body>
</html>

UPDATE

As discussed in the comments: when I do not use the Formatter like above, I get a 400 Bad Request error. This is the header data of the POST request. In this case I tried selecting two roles (id 1 and 3 as you can see below)

Request URL:http://localhost:8080/projects/save 
Request Method:POST 
Status Code:400 Bad Request 
Remote Address:[::1]:8080 
Referrer Policy:no-referrer-when-downgrade 
Response Headers 
Connection:close 
Content-Language:en-GB 
Content-Length:350 
Content-Type:text/html;charset=ISO-8859-1 
Date:Tue, 31 Oct 2017 20:10:09 GMT 
Server:Apache-Coyote/1.1 
Request Headers 
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 
Accept-Encoding:gzip, deflate, br Accept-Language:en-GB,en;q=0.9,en-US;q=0.8,fr;q=0.7 
Cache-Control:max-age=0 
Connection:keep-alive 
Content-Length:161 
Content-Type:application/x-www-form-urlencoded 
Host:localhost:8080 
Origin:http://localhost:8080 
Referer:http://localhost:8080/projects/add 
Upgrade-Insecure-Requests:1 
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/62.0.3202.75 
Safari/537.36 
Form Data 
id: 
name:Implement recipe site 
description:description 
status:RUNNING 
rolesNeeded:1
_rolesNeeded:on
_rolesNeeded:on 
rolesNeeded:3
_rolesNeeded:on
_rolesNeeded:on
14
  • th:id on checkbox is not needed th field is sufficient Commented Oct 31, 2017 at 19:36
  • are your roles shared across multiple project or every project has itsnown roles?(thus therr can be eg multiple leaders in roles table but with different id)? Commented Oct 31, 2017 at 19:37
  • normally if you use domain objects only you domt need a formatter Commented Oct 31, 2017 at 19:38
  • The roles are shared across all projects. Commented Oct 31, 2017 at 19:39
  • I tried replacing th:value=${{role}} with th:value=${role.id} which should normally not use the formatter. Interestingly the result is the same, the values are passed to the controller but are not displayed correctly when editing an existing project. Other project fields display correctly. Commented Oct 31, 2017 at 19:52

2 Answers 2

2

Compleately remove formatter as you dont need it in your case and do the checkbox like that

<input type="checkbox" th:value="${role.id}" th:field="*{rolesNeeded}" th:text="${role.name}"/>

this should work. Id from checkbox will be autointerpreted as existing entities id and will be fetched from the database.

Formatters are meant to generate localized presentation of some objects not to be converters between web forms and backing beans. Yes i am aware that some tutorials are teaching pplmto do that but please dont. Maybe someday in the past, in older versions of spring or thymeleaf it was the correct solution,but right now it is more like a hack, not a how-o-do-thigs-right pattern.

PS: this is a part of working application

Controller method declaration:

public String addPlacePost(@Valid final Place place, BindingResult placeValidation, Model model) {

Checkbox markup:

<fieldset th:object="${place}" th:classappend="${#fields.hasErrors('services')} ? 'has-error' : _ ">
        <legend>Select services</legend>
        <div class="checkbox" th:each="service : ${allServices}">
            <label> <input th:value="${service.id}" th:field="*{services}" type="checkbox"/> <span
                    th:text="${service.name}" th:remove="tag"> </span>
            </label>
        </div>
        <span class="help-block" th:each="msg : ${#fields.errors('services')}" th:text="${msg}">Some error message for this field</span>
    </fieldset>

And the Place entity part that contains Services

@ManyToMany
    @JoinTable(joinColumns = @JoinColumn(name = "place_id"), inverseJoinColumns = @JoinColumn(name = "service_id"))
    @NotEmpty
    private Set<Service> services;

Works like charm both for adding new places as well as editing existing ones.

Sign up to request clarification or add additional context in comments.

4 Comments

I hear you. I'm not using Thymeleaf 3, I've been using spring boot starter (this is a learning project) which includes an earlier version. I now assume my issues have to do with the older version. I'll change my dependencies and config to work with Thymeleaf 3.
Ahh crap, ye this may be the case. Code I have shown you is for thymeleaf 3 with spring boot 1.*. You would need to add couple dependencies and do some tweaks as I recall. I started learning thymeleaf a month ago maybe and thought there is no point in lerning from older versions. I know that thymeleaf can be a pain in the beginning, Iv been trough this.
OK so I configured Thymeleaf 3 and changed my code. I no longer have a Bad Request error but the object returned by the form doesn't contain any roles no matter how many checkboxes I check. I probably still have some minor issue somewhere, maybe with the Thymeleaf config? Or the build? I will mark this as answered since you showed me the correct documented way to do this and open another question with the new issue, otherwise I think this question will get too confusing. Thank you for your help!
Sure no prob, looking forward to see another issue.
1

Running ahead - a complete Github example with explanations is available here -> https://stackoverflow.com/a/46926492/6332774

Now comments specific to your case:

If you did not get it resolved please check this response from Thymeleaf team:

http://forum.thymeleaf.org/The-checked-attribute-of-the-checkbox-is-not-set-in-th-each-td3043675.html

In your case your getRolesNeeded() method needs to return an Array where size of the array needs to equal to number of checkboxes.

For this you can use AttributeConverter as documented here https://stackoverflow.com/a/34061723/6332774

Add StringListConverter class (as in link above) and change your model class as:

@Convert(converter = StringListConverter.class)
private List<String> rolesNeeded = new ArrayList<>();
...
public List<String> getRolesNeeded() {
    return rolesNeeded;
}

public void setRolesNeeded(List<String> rolesNeeded) {
    this.rolesNeeded = rolesNeeded;
}

Then in your html checkbox input remove Id as suggested by @Antoniossss . Change it to something like this:

            <div th:each="roles : ${allRoles_CanAddRolesArrayInController}">
                <input type="checkbox" th:field="*{rolesNeeded}"  th:value="${role.id}"/><label th:text="${role.name}">Role1</label>
            </div>

Hope it helps.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.