Nick Nagorski
Nick Nagorski

Reputation: 115

How to fill out fields in one form for multiple objects in Thymeleaf?

I have a following models (without getters and setters for readability):

   @Entity
public class Recipe extends BaseEntity {
  private String name;
  private String description;
  private Category category;

  @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
  private List<Ingredient> ingredients;

  @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
  private List<Instruction> instructions;

  @ManyToMany
  private List<User> administrators;

  private int preparationTime;
  private int cookTime;

  public Recipe(){
    super();
    ingredients = new ArrayList<>();
    instructions = new ArrayList<>();
    administrators = new ArrayList<>();
  }

  public Recipe(String name, String description, Category category, int preparationTime, int cookTime) {
    this();
    this.name = name;
    this.description = description;
    this.category = category;
    this.preparationTime = preparationTime;
    this.cookTime = cookTime;
  }

*

@Entity
public class Ingredient extends BaseEntity {
  private String name;
  private String condition;
  private double quantity;
  private Measurement measurement;

  @ManyToOne
  private Recipe recipe;

  public Ingredient(){
    super();
  }

  public Ingredient(String name, String condition, double quantity, Measurement measurement) {
    this();
    this.name = name;
    this.condition = condition;
    this.quantity = quantity;
    this.measurement = measurement;
  }

*

@Entity
public class Instruction extends BaseEntity {
  private String name;
  private String description;

  @ManyToOne
  private Recipe recipe;

  public Instruction(){
    super();
  }

  public Instruction(String name, String description) {
    this();
    this.name = name;
    this.description = description;
  }

What i need to do is to fill out fields for each object in one Thymeleaf form and POST it. I know how to do it with a single object. Please explain how to set up the from and controller for multiple objects, so in the end ill have recipe posted with ingredients and instructions list. Thanks!

EDITED: Here is a controller methods:

     @RequestMapping("/recipes/add")
  public String formNewRecipe(Model model) {
    Recipe recipe = new Recipe();

    if (!model.containsAttribute("recipe")) {
      model.addAttribute("recipe", recipe);
    }
    model.addAttribute("action", "/recipes");
    model.addAttribute("heading", "New Recipe");
    model.addAttribute("submit", "Save");
    model.addAttribute("categories", Category.values());
    model.addAttribute("measurements", Measurement.values());
    return "edit";
  }

  @RequestMapping(value = "/recipes", method = RequestMethod.POST)
  public String addRecipe(@Valid Recipe recipe,
                          BindingResult result,
                          RedirectAttributes redirectAttributes) {

    if (result.hasErrors()) {
      redirectAttributes
          .addFlashAttribute("org.springframework.validation.BindingResult.recipe", result);
      redirectAttributes.addFlashAttribute("recipe", recipe);
      return "redirect:/recipes/add";
    }

    recipes.save(recipe);
    redirectAttributes.addFlashAttribute("flash",
        new FlashMessage("New Recipe Created!!!", FlashMessage.Status.SUCCESS));
    return "redirect:/recipes/" + recipe.getId();
  }

and Thymeleaf form:

<form th:action="@{${action}}" method="post" th:object="${recipe}">

                <div class="grid-100 row controls">
                    <div class="grid-50">
                        <h2 th:text="${heading}"></h2>
                    </div>
                    <div class="grid-50">
                        <div class="flush-right">
                            <input class="button" type="submit" th:value="${submit}"/>
                            <a th:href="@{|/recipes|}" class="secondary">
                                <button class="secondary">Cancel</button>
                            </a>
                        </div>
                    </div>
                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Name </label>
                        </p>
                    </div>
                    <div class="grid-40">
                        <p><input type="text" th:field="*{name}"/>
                        <div class="error-message"
                             th:if="${#fields.hasErrors('name')}"
                             th:errors="*{recipe.name}">
                        </div>
                        </p>
                    </div>
                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Description </label>
                        </p>
                    </div>
                    <div class="grid-40">
                        <p><textarea rows="4" th:field="*{description}"></textarea>
                        <div class="error-message"
                             th:if="${#fields.hasErrors('description')}"
                             th:errors="*{recipe.description}">
                        </div>
                        </p>
                    </div>
                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Category </label>
                        </p>
                    </div>
                    <div class="grid-30">
                        <p>
                            <select th:field="*{category}">
                                <option value="" disabled="disabled">Recipe Category</option>
                                <option th:each="c : ${categories}"
                                        th:value="${c.name}"
                                        th:text="${c.name}">All Categories</option>
                            </select>
                        </p>
                    </div>
                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Prep Time </label>
                        </p>
                    </div>
                    <div class="grid-20">
                        <p>
                            <input type="number" th:field="*{preparationTime}"/>
                        <div class="error-message"
                             th:if="${#fields.hasErrors('preparationTime')}"
                             th:errors="*{preparationTime}"></div>
                        </p>
                    </div>
                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Cook Time </label>
                        </p>
                    </div>
                    <div class="grid-20">
                        <p>
                            <input type="number" th:field="*{cookTime}"/>
                        <div class="error-message"
                             th:if="${#fields.hasErrors('cookTime')}"
                             th:errors="*{cookTime}"></div>
                        </p>
                    </div>
                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Ingredients </label>
                        </p>
                    </div>
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Item </label>
                        </p>
                    </div>
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Condition </label>
                        </p>
                    </div>
                    <div class="grid-15">
                        <p class="label-spacing">
                            <label> Quantity </label>
                        </p>
                    </div>
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Measurement </label>
                        </p>
                    </div>

                    <div class="ingredient-row">
                        <div class="prefix-20 grid-20">
                            <p>
                                <input type="text" th:field="*{ingredients[0].name}"/>
                            <div class="error-message"
                                 th:if="${#fields.hasErrors('ingredients[0].name')}"
                                 th:errors="*{ingredients[0].name}"></div>
                            </p>
                        </div>
                        <div class="grid-20">
                            <p>
                                <input type="text" th:field="*{ingredients[0].condition}"/>
                            <div class="error-message"
                                 th:if="${#fields.hasErrors('ingredients[0].condition')}"
                                 th:errors="*{ingredients[0].condition}"></div>
                            </p>
                        </div>
                        <div class="grid-15">
                            <p>
                                <input type="number" th:field="*{ingredients[0].quantity}"/>
                            <div class="error-message"
                                 th:if="${#fields.hasErrors('ingredients[0].quantity')}"
                                 th:errors="*{ingredients[0].quantity}"></div>
                            </p>
                        </div>
                        <div class="grid-20">
                            <p>
                                <select th:field="*{ingredients[0].measurement}">
                                    <option value="" disabled="disabled">Measurement</option>
                                    <option th:each="i : ${measurements}"
                                            th:value="${i.name}"
                                            th:text="${i.name}">Unknown
                                    </option>
                                </select>
                            </p>
                        </div>
                    </div>

                    <div class="prefix-20 grid-80 add-row">
                        <p>
                            <button>+ Add Another Ingredient</button>
                        </p>
                    </div>

                </div>
                <div class="clear"></div>

                <div class="grid-100 row">
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Instructions </label>
                        </p>
                    </div>
                    <div class="grid-20">
                        <p class="label-spacing">
                            <label> Steps </label>
                        </p>
                    </div>
                    <div class="grid-60">
                        <p class="label-spacing">
                            <label> Description </label>
                        </p>
                    </div>

                    <div class="instruction-row">
                        <div class="prefix-20 grid-20">
                            <p>
                                <input type="text" th:field="*{instructions[0].name}"/>
                            <div class="error-message"
                                 th:if="${#fields.hasErrors('instructions[0].name')}"
                                 th:errors="*{instructions[0].name}"></div>
                            </p>
                        </div>
                    </div>

                    <div class="instruction-row">
                        <div class="grid-50">
                            <p>
                                <input type="text" th:field="*{instructions[0].description}"/>
                            <div class="error-message"
                                 th:if="${#fields.hasErrors('instructions[0].description')}"
                                 th:errors="*{instructions[0].description}"></div>
                            </p>
                        </div>
                    </div>

                    <div class="prefix-20 grid-80 add-row">
                        <p>
                            <button>+ Add Another Step</button>
                        </p>
                    </div>

                </div>
                <div class="clear"></div>

                <div class="row">&nbsp;</div>
            </form>

Upvotes: 2

Views: 14092

Answers (1)

Metroids
Metroids

Reputation: 20477

It looks something like this:

<form th:object="${recipe}">
    <input type="text" th:field="*{ingredients[0].name}" />
    <input type="text" th:field="*{ingredients[0].description}" />

    <input type="text" th:field="*{instructions[1].name}" />
    <input type="text" th:field="*{instructions[1].description}" />
</form>

If you have a dynamic amount of ingredients, the th:each might look like this:

 <th:block th:each="ingredient,i : ${recipe.ingredients}">
    <input type="text" th:field="*{ingredients[__${i.index}__].name}" /><br />
    <input type="text" th:field="*{ingredients[__${i.index}__].condition}" /><br />
    <input type="text" th:field="*{ingredients[__${i.index}__].quantity}" /><br />
    <input type="text" th:field="*{ingredients[__${i.index}__].measurement.anotherField}" /><br />
 </th:block>

Dynamically adding another ingredient to the form is kind of painful... you either have to submit the form and modify the Recipe object in a controller (adding an ingredient, then redirecting back to the form). Or you can use javascript to copy the fields, making sure the name/id/etc match the others, with the index incremented.

Upvotes: 5

Related Questions