MooMoo MooMoo - 4 months ago 82
Java Question

Spring MVC / JSP - How to pass nested list of object to controller from a select form

I am using Spring MVC with JSP.

I have a User entity which contains a list of Group entities. The relationship is Many-to-Many (a Group entity contains a list of User entities).

In my user controller I have a method for returning the add user page with an empty User entity and a list of available Group entities.

@RequestMapping(value = "/add", method = RequestMethod.GET)
public ModelAndView loadUserAdd() {
ModelAndView mv = new ModelAndView("user/userAdd");
mv.addObject("user", new User());
try {
mv.addObject("groups", gr.listGroups());
} catch (TestException e) {
mv.addObject("error", e.getMessage());
e.printStackTrace();
}

return mv;
}


On the userAdd page, I want to select the group(s) the user will have from the list of the available groups.

<div class="row">
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6">
<select id="availableGroups" class="form-control" multiple onclick="shuttle('availableGroups', 'selectedGroups')">
<c:forEach items="${groups}" var="group">
<option value="${group.id}">${group.id}: ${group.name}</option>
</c:forEach>
</select>
</div>
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6">
<select id="selectedGroups" class="form-control" multiple onclick="shuttle('selectedGroups', 'availableGroups')">
<c:forEach var="group" items="${user.groups}" varStatus="status">
<option value="${group.id}">${group.id}: ${group.name}</option>
</c:forEach>
</select>
</div>
</div>


Note, the 'shuttle' function moves a group from one select element to the other. E.g. from the available groups to the selected groups or vice versa. This works.

On submit, I want to have the selected groups set in the user entity so that it will arrive in the addUser method.

@RequestMapping(value = "/add", method = RequestMethod.POST)
public ModelAndView addUser(@ModelAttribute("user") User user) {


Instead on submit, the user entity contains a null list of groups. I'm sure my JSP is wrong so it would be great if someone could point me in the right direction. Any advice on improvements would be good as I'm doing this as a learning exercise. Thanks.

Answer

A college suggested a way to solve this question. The solution offered uses a single select field rather than two select fields with a shuttle moving values from available to selected and vice versa.

In the JSP, I've replaced both select fields with just one:

<div class="row">
    <div class="col-xs-3 col-sm-3 col-md-3 col-lg-3">
        Groups
    </div>
    <div class="col-xs-9 col-sm-9 col-md-9 col-lg-9">
        <form:select
                path="groups"
                items="${availableGroups}"
                multiple="true"
                itemValue="id"
                itemLabel="name"
                class="form-control" />
    </div>
</div>

This form:select iterates over the available groups and creates a option element for each. The select attributes are:
path - uses the value 'groups' to map to the getGroups getter method on the user entity in the view model (modelAndView.addObject("users", users);). items - is the list of all available groups in the system. This is set in the model from the controller. See note about this below. itemValue - is the value which will become the select's option's value. The 'id' maps to the getId getter method of the current group. itemLabel - is the value which will become the select's option's visible label. The 'name' maps to the getName getter method of the current group. The form:select also marks options as selected if the user has them set.

Here is an example output where the groups are 1,2,3,4 (both in id and name) and the user has group 1,2.

  <div class="col-xs-9 col-sm-9 col-md-9 col-lg-9">
    <select id="groups" name="groups" class="form-control" multiple="multiple">
      <option value="1" selected="selected">1</option>
      <option value="2" selected="selected">2</option>
      <option value="3">3</option>
      <option value="5">4</option>
    </select>
    <input type="hidden" name="_groups" value="1"/>
  </div>

To make 'availableGroups' available I've used:

@ModelAttribute("availableGroups")
public List<Group> initializeGroups() {
    return us.listGroups();
}

This makes the list of available-groups available to each view of the controller.

On submit, to update the user with the selected groups (either adding or removing groups as this works on edit too), I've used a Converter (org.springframework.core.convert.converter.Converter).

@Component
public class GroupConverter implements Converter<String, Group> {
    @Autowired
    GroupService groupService;
    public User convert(String element) {
        User user = null;
        if(element != null) {
            int id = Integer.parseInt(element);
            user = userService.fetchUser(id);
        }
        return user;
    }
}

The Converter takes the id of the group and gets it from the data source then magically sets it in the user entity before the controller is called.

Converters are set on the WebMvcConfigurationSupport...

@Autowired
GroupConverter groupConverter;

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(groupConverter);
}
Comments