JavaTechnical
JavaTechnical

Reputation: 9357

How to map HashMap keys to a select in thymeleaf?

I have a HashMap which looks pretty much like this..

class MyDTO
{
private Map<Long, StringListWrapper> myMap=new HashMap<>();
private List<Long> keys=new ArrayList<>();
private List<String> values=new ArrayList<>();
// setter and getter methods
}
class StringListWrapper
{
private List<String> st=new ArrayList<>();
// setter and getter methods
}

Now, for the map keys, I have a select which will contain the keys, and a list box which will be used for values.

<select th:field="*{myMap}">
<option th:each="key : *{keys} th:value="${key}" th:text="${key}"></option>
</select>

Here above, keys refer to the keys in MyDTO. I used it to show the keys in the <select>. However, this select must be mapped to the myMap to make sure that the option selected here acts as a key.

<select multiple="multiple">
<option th:each="value : *{values}" th:value="${value}" th:text="${value}"></option>
</select>

Now, also this above multiple select must be mapped to myMap so that the options selected here get into the StringListWrapper.st. Now, how and where to put the th:field attribute for multiple select above?

Thanks in advance. Hope you will reply as soon as possible.

Upvotes: 1

Views: 9354

Answers (1)

martian111
martian111

Reputation: 593

There are a few things unclear about the question, but based on the code, I'm assuming that the myMap property is a Map because you will be providing multiple pairs of these select fields for uses to select the values for some number of keys.

If the assumption above is false, then why have myMap as a Map instead of two different properties/attributes of MyDTO?

Going with my assumption, you'll need the select pairs to be converted into a List or array in MyDTO and have a getter (getMyMap()) that creates a Map based of the list. The entry in the list above would be a simple value object with Long key and List<String> as its properties. Here is an working example of this use case. See how this is transformed by Thymeleaf and you might find a tweaked version that works with your solution.

Also, minor thing: IMHO, I don't believe it's appropriate to have the keys and values properties be within MyDTO, though it might just be simplified for this question only. They should be model attributes instead since they are not user inputs of the form. It technically works but doesn't strictly adhere to separation of concerns.

References/Credits: Dynamic Form Fields - http://www.thymeleaf.org/doc/thymeleafspring.html#dynamic-fields

Spring Controller:

package net.martian111.examples.spring.web;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/public/stackoverflow/q26181188")
public class StackoverflowQ26181188Controller {

    public static class Entry {
        private Long id;

        private List<String> values;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public List<String> getValues() {
            return values;
        }

        public void setValues(List<String> values) {
            this.values = values;
        }

    }

    public static class FormBackingBean {
        List<Entry> entries;

        public List<Entry> getEntries() {
            return entries;
        }

        public void setEntries(List<Entry> entries) {
            this.entries = entries;
        }

        public Map<Long, List<String>> getMyMap() {
            Map<Long, List<String>> map = new HashMap<>();
            for (Entry entry : entries) {
                // StringListWrapper constructed from entry.getValues() here...
                map.put(entry.getId(), entry.getValues());
            }
            return map;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            int i = 1;
            for (Entry entry : entries) {
                sb.append("Pair #" + i + ": ID=" + entry.getId() + ", Values="
                        + entry.getValues() + "\n");
                ++i;
            }
            return sb.toString();
        }
    }

    // Can be set within the @RequestMapping methods too (mv.addObject())
    @ModelAttribute("keys")
    public List<Long> getKeys() {
        return Arrays.asList(null, 1L, 2L, 3L);
    }

    @ModelAttribute("values")
    public List<String> getValues() {
        return Arrays.asList(null, "abc", "def", "ghi");
    }

    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView get() {
        ModelAndView mv = new ModelAndView("stackoverflow/q26181188");
        // Blank Form Backing Bean
        FormBackingBean fbb = new FormBackingBean();
        fbb.setEntries(Arrays.asList(new Entry(), new Entry(), new Entry(),
                new Entry(), new Entry()));
        mv.addObject("fbb", fbb);
        return mv;
    }

    @RequestMapping(method = RequestMethod.POST)
    public ModelAndView post(FormBackingBean fbb) {

        ModelAndView mv = new ModelAndView("stackoverflow/q26181188");
        mv.addObject("fbb", fbb);

        // Print Form Backing Bean
        System.out.println("FBB: \n" + fbb);

        // Redisplay submitted from
        return mv;
    }
}

Thymeleaf Template:

    <div th:each="entry,entryStat : *{entries}">
      Pair #<span th:text="${entryStat.count}">1</span>
      <select th:field="*{entries[__${entryStat.index}__].id}">
        <option th:each="key : ${keys}" th:value="${key}" th:text="${key}"></option>
      </select>
      <select multiple="multiple" th:field="*{entries[__${entryStat.index}__].values}">
        <option th:each="value : ${values}" th:value="${value}" th:text="${value}"></option>
      </select>
    </div>

    <button type="submit" name="submit" class="btn btn-primary">Submit</button>

  </form>
</body>
</html>

EDIT: Additional details in response to the first comment Both Map and List properties can be mapped to a collection of HTML form fields. Each Map.Entry or list element is mapped to a single HTML form field, with the name in the form propertyName[index], where index is the integer index of the element for the case of a List, or is the key value of the entry for the case of a Map. The solution above illustrates this for the List case.

To illustrate the Map case, say you want an HTML form to result in a myMap with the following contents:

123L : ["abc", "def"]
234L : ["abc", "ghi"]

Working backwards, the query string (before URL encoding) required for Spring MVC to create the Map above will need to look like: myMap[123]=abc&myMap[123]=def&myMap[234]=abc&myMap[234]=ghi. To get a browser to submit that query string, the HTML form will have to have two multi <select> form elements, one with name="myMap[123]" and the other with name="myMap[234]". However, the name of form elements cannot be set by another form field in standard HTML. In other words, there is no th:field value for the key <select> elements to do this (answering this Stackoverflow question).

With that said, an outside-the-box solution would be client side JavaScript scripting that gathers the necessary data from the form fields and create the required query string to submit the form. It would be a SO different question for a different audience, but I feel that would be an unnecessarily complex and specialized. Also, whereas the solution above works both to generate the HTML view from MyDTO and back to MyDTO from a form submission using the same HTML form, a JavaScript solution will require distinct specialized code for each direction.

Upvotes: 2

Related Questions