Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Java

How to add an object and then another object as an attribute to the first object in a Thymeleaf template?

Hey I arrived at the last Java Web Dev Techdegree project and am having trouble to create a Thymeleaf template, specifically to bind a nested object.

My code basically looks like this:

<form th:object="${recipe}">
  <input th:field="*{name}"/>
  <input th:field="*{description}"/>
  <input th:field="*{cookTime}"/>
  ...
  <select th:field="*{ingredients}"/>
    <option th:each="ingredient : ${ingredients}" th:value="${ingredient.name}"></option>
  <input th:field="*{ingredients.condition}"/>
  <input th:field="*{ingredients.quantity}"/>
  ...
  <input th:field="*{steps}"/>
</form>

So I am trying to bind these values to a Recipe object. Everything but the binding of the ingredient looks fine.

Can you help me with this? How am I to create an Ingredient with name, condition and quantity and bind it to the "ingredients" list of the recipe object.

Kind Regards, Florian

4 Answers

Hi, Florian Tönjes

I think there is some problem with this select thing.

If the Item can be selected, that means we should have list of Items somewhere in database to put into this select.

That also means that we should make something like this:

    @Autowired
    private itemsService itemsService; // with all DAOs and all other stuff

    @RequestMapping("/add")
    public String recipeForm(Model model) {
        model.addAttribute("recipe", new Recipe());
        model.addAttribute("ingredients", ingredientService.findAll());
        model.addAttribute("categories", Category.values());

        model.addAttribute("items", itemsService.findAll());
        return "edit";
    }

    // same thing for edit
    @RequestMapping("/recipe/{id}/edit")
    public String editForm(@PathVariable Long id, HttpServletRequest request, Model model) {
        Recipe recipe = recipeService.findById(id);
        model.addAttribute("recipe", recipe);
        model.addAttribute("categories", Category.values());

        model.addAttribute("items", itemsService.findAll());
        return "edit";
    }

In this case Thymeleaf can be easily changed to this one:

                      <select th:field="*{ingredients[__${ingredientStat.index}__].name}">
                        <option
                                th:each="item : ${items}"
                                th:value="${item.id}"
                                th:text="${item.name}"
                        >
                        </option>
                      </select>

Of course item.id and item.name will depend on your implementation.

If you want to test whether this works, push artificial array with Strings to edit method in Controller:

    // same thing for edit
    @RequestMapping("/recipe/{id}/edit")
    public String editForm(@PathVariable Long id, HttpServletRequest request, Model model) {
        Recipe recipe = recipeService.findById(id);
        model.addAttribute("recipe", recipe);
        model.addAttribute("categories", Category.values());

        // mocking items as list of strings
        List<String> items = new ArrayList<String>;
        items.add("item 1");
        model.addAttribute("items", items);
        return "edit";
    }

and then in Thymeleaf template you can push

                      <select th:field="*{ingredients[__${ingredientStat.index}__].name}">
                        <option
                                th:each="item : ${items}"
                                th:value="${item}"
                                th:text="${item}"
                        >
                        </option>
                      </select>

I tried that : it has worked.

You probably ask me: how I will go

I will probably make Item as separate Class, because it is a good thing to have common ingredients for all recipes. May be Items can be specific for user or may be common to everyone.

Again I don't know what they have meant. I will definitely ask them but on Monday. Craig and Chris they don't reply much on weekend. But I will tell you they usually say : go harder way. So probably they have meant that items can be shared across recipes...

But you can proceed now. I hope I made myself clear. Let me know if that worked for you.

Regarding your code ...

As you see select works like this in Thymeleaf: you put in <select> tag field that you want to fill: in this case :

<select th:field = "*{ingredients[__${ingredientStat,index}__].name}">

Again select is used for name only, quantity and Condition are input fields and I posted how to fill them.

What you are doing is to try to select Ingredient ... which is not right I think.

I think we do not select Ingredient. We select Item which is ingredient.name, we input condition and quantity and that is how the whole object is send to Controller, does it make sense ?

I'm posting again my whole part related to ingredient:

           <div th:each="ingredient : *{ingredients}" class="ingredient-row">
                <div class="prefix-20 grid-30">
                    <input hidden th:field="*{ingredients[__${ingredientStat.index}__].id}"/>
                    <input hidden th:field="*{ingredients[__${ingredientStat.index}__].version}"/>
                    <p>
                      <select th:field="*{ingredients[__${ingredientStat.index}__].name}">
                        <option
                                th:each="item : ${items}"
                                th:value="${item}"
                                th:text="${item}"
                        >
                        </option>
                      </select>
                        <!--<input th:field="*{ingredients[__${ingredientStat.index}__].name}"/>-->
                    </p>
                </div>
                <div class="grid-30">
                    <p>
                        <input th:field="*{ingredients[__${ingredientStat.index}__].condition}"/> </input>
                    </p>
                </div>
                <div class="grid-10 suffix-10">
                    <p>
                        <input th:field="*{ingredients[__${ingredientStat.index}__].quantity}"/> </input>
                    </p>
                </div>
            </div>
  • So we have big div with th:each looping through ingredient.
  • Inside each ingredient we assign everything easily, except ingredient.name field
  • Ingredient.name is assigned through select using external items array, that is sent to our model
  • in <select> we specify field that we are filling, in <option> we list through items not related to ingredient, and post value as item.name or can be item.id depends on the implementation

P.S. The most important thing I wanted to add, that this project is freelance...

Considering you don't have actual Product owner, that gives you actual orders of what he wants, and don't have access to Slack, and they also are not that strict in requirements, you can solve problems in your own way.

If you feel that this select too much, leave Ingredient for now with all <input text> fields. It is first step towards better solution.

The most important thing is to go always further.

You can save unknown thing for TODO and then figure out the problem later.

Believe me I have ton of unaswered problems that I don't have answers to, and probably no one will.

Important thing is to do something little by little, save TODO problems for later, and then if possible come back and resolve them.

And I'm not quite sure what do you mean by

how do I grab an ingredient out of this? The ingredientStat.index stuff only work when it is within a th:each, right? But the only th:each is at the option field like this:

This you have to explain me more, if you feel my solution does not fit.

May be I haven't understood the tasks properly...

Not an answer, because I didn't get where you are now. But briefly looking at problem, I think there are two solutions for your problem:

  1. "Dynamic Fields" :

http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#dynamic-fields

I haven't tried that one in thymeleaf, but I think it does exactly what you want: adding new field or removing it. When I get where you are I'll certainly try that.

Another solution is JavaScript. Remember like TODOs in Angular? Unfortunately I'm not that good with JS to find easily solution adaptable to this particular problem, but feel this place is exactly where JS can be of help...

Hopefully you'll be there soon :) . I already posted on stack overflow and the thymeleaf forum and haven't gotten an answer, yet.

Hi, Florian Tönjes

I've looked at your question more closer, and I saw that the question I was trying to answer you above, was misinterpreted by me.

If all that you wanted was to combine Ingredients fields and pass them from Thymeleaf to controller, here is how your Thymeleaf should look like:

            <div th:each="ingredient : *{ingredients}" class="ingredient-row">
                <div class="prefix-20 grid-30">
                    <p>
                     <input hidden th:field="*{ingredients[__${ingredientStat.index}__].id}"
                     />
                     <input hidden th:field="*{ingredients[__${ingredientStat.index}__].version}"
                      />
                        <input th:field="*{ingredients[__${ingredientStat.index}__].name}"
                               name="item"/>
                    </p>
                </div>
                <div class="grid-30">
                    <p>
                        <input th:field="*{ingredients[__${ingredientStat.index}__].condition}"
                               name="condition"> </input>
                    </p>
                </div>
                <div class="grid-10 suffix-10">
                    <p>
                        <input th:field="*{ingredients[__${ingredientStat.index}__].quantity}"
                               name="quantity"> </input>
                    </p>
                </div>
            </div>

I tried on your 'Ham and Eggs' recipe edit page, and your only Ingredient was filled correctly in HTML edit page. I also tried with two ingredients: worked as well

Try that out and let me know if that is what you've wanted...

You probably should be able to finish stuff on your own from here. In Controller you should be able to access Recipe.ingredients array ...

I changed your RecipeController, to see if they went through to this one:

    @RequestMapping(value = "/index", method = RequestMethod.POST)
    public String addRecipe(@Valid Recipe recipe, BindingResult result,
                            HttpServletRequest request
            ) {

        recipe.getIngredients().forEach(
                ingredient -> System.out.println(ingredient.getId())
        );
        recipe.getIngredients().forEach(
                ingredient -> System.out.println(ingredient.getVersion())
        );
        recipe.getIngredients().forEach(
                ingredient -> System.out.println(ingredient.getName())
                );
        recipe.getIngredients().forEach(
                ingredient -> System.out.println(ingredient.getCondition())
        );
//        recipeService.save(recipe);
        return "redirect:/index";
    }

And all changed values were printed for me.

The syntax may look strange but it used throughout the Thymeleaf documentation.

You are basically going through each ingredient available in recipe, and for each recipe use index directly to put it in th:field.

Somewhat like this:

  • ingredients[0].name = ...
  • ingredients[0].condition = ...
  • ....

If you want to know why it does work, I cannot tell you for sure and probably there are other ways to do it.

I know that this is what I was able to do with Thymeleaf docs. If you find better way, let me know...

Let me know if this will work for you..

Oh and this policy should work with Step class and input, too

PS. The answer above was about adding new Ingredient or Step, using the button, that's why I posted that answer. I'll remove it, if this one will work for you.

Going to test this now. Thanks again ;-)

Kind Regards, Florian

Hi, Florian Tönjes

I forgot to mention the most important thing: I switched to HTML5 Thymeleaf processing. The above code will not work with your current setting.

I suggest you do this, change your TemplateConfig file function

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        final SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setCacheable(false);
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode("LEGACYHTML5"); // removes this stupid errors
    return templateResolver;
    }

Include this dependency in your gradle file:

    compile 'net.sourceforge.nekohtml:nekohtml:1.9.21'

Rebuild gradle, and your HTML should be HTML5 compliant now

I am still having problems with this code:

                <div class="ingredient-row">
                  <div class="prefix-20 grid-30">
                    <p>
                      <select>
                        <option value="">Select Item</option>
                      </select>
                    </p>
                  </div>
                  <div class="grid-30">
                    <p>
                      <input> </input>
                    </p>
                  </div>
                  <div class="grid-10 suffix-10">
                    <p>
                      <input> </input>
                    </p>
                  </div>
                </div>

how do I grab an ingredient out of this? The ingredientStat.index stuff only work when it is within a th:each, right? But the only th:each is at the option field like this:

                <div class="ingredient-row">
                  <div class="prefix-20 grid-30">
                    <p>
                      <select>
                        <option th:each="ingredient : ${ingredients}" th:text="${ingredient.name}" value="">Select Item</option>
                      </select>
                    </p>
                  </div>
                  <div class="grid-30">
                    <p>
                      <input> </input>
                    </p>
                  </div>
                  <div class="grid-10 suffix-10">
                    <p>
                      <input> </input>
                    </p>
                  </div>
                </div>

I guess I have to wait to see your particular solution :)

I haven't seen <select> tag. I will try with select and post with it

Thank you Alexander.