Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update form: Spring mvc + thymeleaf

I'm trying to create a thymeleaf form to update just a couple of attributes of the backing object:

@RequestMapping(value = "/jobs/{id}", method = RequestMethod.GET)
public ModelAndView update(@PathVariable Integer id ) {
    ModelAndView mav = new ModelAndView("updateJob.html");
    JobDescription updateJob = jobDescriptionService.findByID(id);
    mav.addObject("updateJob", updateJob);
    return mav;
}

@RequestMapping(value = "/jobs/{id}", method = RequestMethod.PUT)
public String saveUpdate(@PathVariable Integer id, @ModelAttribute("updateJob") JobDescription updateJob) {
    jobDescriptionService.update(updateJob);
    return "redirect:/jobs/" + id;
}


<form th:action="@{'/jobs/'+ ${updateJob.id}}" th:object="${updateJob}" th:method="PUT">
    <table>
        <tr>
            <td><label>Description</label></td>
            <td><input type="text" th:field="*{description}" /></td>
        </tr>
        <tr>
            <td><label>Deadline</label></td>
            <td><input type="text" th:field="*{deadline}" /></td>
        </tr>
        <tr>
            <td></td>
            <td><button type="submit">Update</button></td>
        </tr>
    </table>
</form>

The problem is that the job object has a couple of other attributes(like id, createdDate, etc) which I don't want to update. However, when I click the submit button of the update form, the object created in the saveUpdate method has those attributes set to null(unless I set them in hidden fields inside the form). Is there any other way I could keep them?

like image 226
Mircea Badescu Avatar asked Nov 27 '14 09:11

Mircea Badescu


2 Answers

I have the same problem as you, so I've built my own solution

1- You need two action on controller : view (GET) and action (POST)

@GetMapping("/user/edit/{userId}")
public ModelAndView editUserView(@PathVariable Long userId) throws NotFoundException {

    User user = this.userService.load(userId);

    if (user == null) {
        throw new NotFoundException("Not found user with ID " + userId);
    }

    ModelAndView modelAndView = new ModelAndView();

    modelAndView.setViewName("user.edit");
    modelAndView.addObject("user", user);

    return modelAndView;
}

@PostMapping("/user/edit/{userId}")
public ModelAndView editUserAction(HttpServletRequest request, @PathVariable Long userId, @Validated(User.ValidationUpdate.class) User userView,
        BindingResult bindingResult) throws Exception {

    User user = this.userService.load(userId);

    if (user == null) {
        throw new NotFoundException("Not found user with ID " + userId);
    }

    ModelAndView modelAndView = new ModelAndView();
    if (bindingResult.hasErrors()) {            
        modelAndView.setViewName("user.edit");
        modelAndView.addObject("user", userView);

        return modelAndView;
    }

    Form.bind(request, userView, user);

    this.userService.update(user);

    modelAndView.setViewName("redirect:/admin/user");

    return modelAndView;
}

2- One view with error display (verry important : add hidden input to send id for validation)

<fieldset th:if="${#fields.hasErrors('${user.*}')}" class="text-warning">
    <legend>Some errors appeared !</legend>
    <ul>
        <li th:each="err : ${#fields.errors('user.*')}" th:text="${err}"></li>
    </ul>
</fieldset>

 <form action="#" th:action="@{/admin/user/edit/{id}(id=${user.id})}" th:object="${user}" method="post">
      <div th:class="${#fields.hasErrors('firstName')} ? 'form-group has-error' : 'form-group'">
          <label class="control-label" for="firstName">First Name <span class="required">*</span></label>
          <input type="text" th:field="*{firstName}" required="required">
      </div>
      ...
    <input type="hidden" th:field="*{id}">
</form>

3- For my example, i wrote a FormUtility class to merge two object :

public static List<String> bind(HttpServletRequest request, Object viewObject, Object daoObject) throws Exception {

    if (viewObject.getClass() != daoObject.getClass()) {
        throw new Exception("View object and dao object must have same type (class) !");
    }

    List<String> errorsField = new ArrayList<String>();

    // set field value
    for (Entry<String, String[]> parameter : request.getParameterMap().entrySet()) {

        // build setter/getter method
        String setMethodName = "set" + parameter.getKey().substring(0, 1).toUpperCase()
                + parameter.getKey().substring(1);
        String getMethodName = "get" + parameter.getKey().substring(0, 1).toUpperCase()
                + parameter.getKey().substring(1);

        try {
            Method getMethod = daoObject.getClass().getMethod(getMethodName);
            Method setMethod = daoObject.getClass().getMethod(setMethodName, getMethod.getReturnType());
            setMethod.invoke(daoObject, getMethod.invoke(viewObject));
        }
        catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException exception) {
            errorsField.add(parameter.getKey());
        }
    }

    return errorsField;
}

Hope this help you.

like image 81
M. Mohamed Avatar answered Jan 03 '23 23:01

M. Mohamed


The safest way to do this is to load original job object by id, and set new values on it and then to update it... Something like:

JobDescription originalJob = jobDescriptionService.findById(updateJob.getId());

originalJob.setParamForUpdate(updateJob.getParamForUpdate());
originalJob.setAnotherParamForUpdate(updateJob.getAnotherParamForUpdate());

jobDescriptionService.update(originalJob);

And this will save all data you want to keep unchanged...

like image 38
Djordje Ivanovic Avatar answered Jan 03 '23 23:01

Djordje Ivanovic