(Alex)minjun yu

© Minjun Yu. All rights reserved.

Spring MVC - Entity Creation And Update Using The Same View

When designing entity creation and update form in a web app, one issue always comes up - should create-entity and update-entity page(assuming there is one entity form per page) look the same or at least similar? I think yes. It will give users a more consistent look and feel, hence more user friendly. A number of beginner developers will create two separate pages for creation and update. In other words, most of the HTML tags are duplicated. It violates the DRY principle.

Entity creation and update do not need two separate views/forms. In this post I am going to demonstrate how to do it with Spring Boot and Thymeleaf. Absolutely no javascript.

Let's design a blog post creation and update logic as an example.

Assumptions & Preperations

  • Spring boot and thymeleaf are correctly setup
  • Familiar with spring controllers and default project structure
  • Familiar with thymeleaf basic syntax

Follow The Steps

Create a thymeleaf template which has the blog post form

<form method="post"
  <div>
    <label for="post-title">
        <input type="text"
               id="post-title"
               class="textbox"
                placeholder="title"/>
    </label>
</div>
<div>
    <label for="post-content">
        <textarea id="post-content"
                  class="textarea"
                  placeholder="content in markdown"></textarea>
    </label>
</div>
<div>
    <button type="submit"></button>
</div>
</form>

I will call it post.html under resources/templates/posts/ directory. We would like to have localhost:8080/posts/create and localhost:8080/posts/{id}/edit both forward to this page. As a result the view (HTML code) is not duplicated.

View Model

public class PostFormBean {
    private Integer id;
    private String title;
    private String content;
    // .. getters/setters
}

Create The Controllers

@Controller
@RequestMapping("/posts")
public class PostsController {
    private final PostFacade postFacade; // this is where business logic takes place. e.g. CRUD operation

    @Autowired
    public PostsController(PostFacade postFacade) {
        this.postFacade = postFacade;
    }
    
    // goes to the post creation view
    @GetMapping(value = "/create")
    public String createPostPage(@ModelAttribute("post") PostFormBean postFormBean){
        return "posts/post"; // same as edit page
    }

    // goes to the post update view
    @GetMapping(value = "/{id}/edit")
    public String editPostPage(Model model, @PathVariable("id") Integer id) {
        PostFormBean postFormBean = postFacade.getPostForEdit(id);
        model.addAttribute("post",postFormBean);
        return "posts/post"; // same as create page
    }

    // submit post creation form
    @PostMapping(value = "create/submit")
    public String createNewPost(@ModelAttribute("post") PostFormBean postFormBean){
        postFacade.createPost(postFormBean);
        return "redirect:/";
    }
    
    // submit the update
    @PostMapping(value = "/{id}/edit/submit")
    public String saveEdit(@ModelAttribute("post") PostFormBean postFormBean){
        postFacade.saveEdit(postFormBean);
        return "redirect:/";
    }
}

Modify The HTML Code

<form th:id="${'post-form'}"
      method="post"
      th:object="${post}"
      <!-- if the post has no id, then the action url becomes "/posts/create/submit", meaning it is a new post -->
      <!-- if the post has an id, then the action url becomes "/posts/{id}/edit/submit", meaning it is an update. {id} in the path is the id of the current post being modified -->
      th:action="@{${post.id == null ? '/posts/create/submit' : '/posts/'+post.id+'/edit/submit'}}">
    <div>
        <label for="post-title">
            <input type="text"
                   id="post-title"
                   class="textbox"
                   th:field="*{title}"
                   placeholder="title"/>
        </label>
    </div>
    <div>
        <label for="post-content">
            <textarea id="post-content"
                      class="textarea"
                      th:field="*{content}"
                      placeholder="content in markdown"></textarea>
        </label>
    </div>
   
    <div>
        <!-- if the post has no id, then it is a new post. The button shows "create"  -->
        <!-- if the post has an id, then it is a post update. The button shows "save edit"  -->
        <button type="submit" th:text="${post.id == null ? 'create':'save edit'}"></button>
    </div>
</form>  

Conclusion

Most of the HTML code is reused because we are using the same page for both creation/update actions. The only significant difference is that the form action url is dynamically generated according to the view model. Happy coding!