Radio Button fields Thymeleaf атрибут th:field заменяет своим значением атрибут th:name в поле с радио кнопками

Атрибут th:field заменяет своим значением атрибут th:name в поле с радио кнопками. Проблема в том что групп с радиокнопками у меня великое множество, а поле в которое они заносят информацию через th:field одно. И получается что можно выбрать только одну из всех радиокнопок, а не одну в каждой группе.

<form action="#" th:action="@{/user/save_answer}" method="post">

<div th:each="question : ${questions}">
    <span th:text="${question.getValueQuestion()}">question</span>

    <ul>
    <li th:each="variant : ${question.getVariants()}">

            <input th:type="${question.choiceType}" th:id="${variant.id}" th:name="${question.getValueQuestion()}" th:field="${user.variants}" th:value="${variant.id}" >
            <label  th:for="${variant.id}" th:text="${variant.getValueVariant()}">variant </label>
    </li>
    </ul>

</div>
   <button type="submit">Complete</button>
</form>

Суть в том что у меня есть анкета с вопросами и в каждом вопросе несколько вариантов ответа. Как видите я реализовал это циклом вариантов ответа внутри цикла вопросов. за счет того th:name = название вопроса - игнорируется и берется th:field = массив с ответами юзера, который у нас для всех вариантов ответа, радиокнопки получается с одним и тем же полем name = поле со вариантами ответов. И тем самым как бы мы можем выбрать только один ответ из всех вариантов, которые присутствуют во всех вопросах.

${user.variations} - в это поле, которое принадлежит сущности USER, мы вводим объекты Variants, чтобы позже мы могли видеть варианты его ответов, класс описан ниже:

@Data
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "email")
    private String email;
    @Column(name = "password")
    private String password;
    @Column(name = "first_name")
    private String firstName;
    @Column(name = "last_name")
    private String lastName;
    @Enumerated(value = EnumType.STRING)
    @Column(name = "role")
    private Role role;
    @Enumerated(value = EnumType.STRING)
    @Column(name = "status")
    private Status status;
    @ManyToMany(cascade = { CascadeType.ALL })
    @JoinTable(
            name = "user_variant",
            joinColumns = { @JoinColumn(name = "user_id") },
            inverseJoinColumns = { @JoinColumn(name = "variant_id") }
    )
    private Set<Variants> variants = new HashSet<>();
}
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private QuestionnairesRepository questionnairesRepository;
    @Autowired
    private QuestionsRepository questionsRepository;
    @Autowired
    private VariantsRepository variantsRepository;
    @Autowired
    private UserRepository userRepository;


    @GetMapping
    public String getAllQuestionnaires(Model model){
        model.addAttribute("questionnaires",questionnairesRepository.findAll());
        return "/user_questionnaires";
    }


    @GetMapping("/questionnaire/{id}")
  //  @PreAuthorize("hasAuthority('developers:read')")
    public String listQuestion(@PathVariable Long id, Model model){

            // получаем авторизированного пользователя
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String currentPrincipalName = authentication.getName();

        Questionnaires questionnaire = questionnairesRepository.findById(id).get();
        model.addAttribute("questionnaire", questionnaire);

        model.addAttribute("user", userRepository.findByEmail(currentPrincipalName).get());

        Iterable<Questions> questionsFromBD = questionsRepository.findAllByQuestionnaire_Id(id);
        model.addAttribute("questions", questionsFromBD);
        return "/user_variants";
    }

    @PostMapping("/save_answer")
    public String saveAnswer(@ModelAttribute("user") User user, Model model){
        System.out.println(user);
        return "/success";
    }
}

изображение анкеты:

изображение анкеты

вот html-код самой страницы, он показывает, что все кнопки имеют одинаковое "name"

enter image description here

Что необходимо исправить что бы для каждого вопроса выбиралась одна радиокнопка и при заполнении варианты ответов заносились в поле вариантов юзера?


Ответы (1 шт):

Автор решения: Михаил Ребров

Thymeleaf всё делает абсолютно верно.
Вы ему указываете поле variants - он его и указывает в атрибуте name.
Ровно то, что вы просили.

Вопрос в том, как добиться того, что вы задумали.

Начнем от обратного.

Что Вы сохраняете?

Согласно контроллеру и методу, который принимает запрос формы - мы сохраняем пользователя.
Но я смотрю на эту страницу(↓↓↓) и не вижу там пользователя.

введите сюда описание изображения

То, что я вижу на странице, похоже на анкету.
И сохранять по идее мы должны не пользователя, а результаты ответов на вопросы анкеты.
Я вижу вопросы, но не вижу ответов).
И в данном случае это не идеома:

  • Я не вижу классов, которые бы описывали ответы.
  • Я не вижу классов, которые бы описывали форму, на которую я смотрю.

Вместо этого мы напрямую связываем пользователя с вариантами, так, как будто мы товар в магазине покупаем:

  • вот у этого пользователя карие глаза, а у того зеленые
  • этот пользователь размера XL, а вот у того S-ка

Это неправильно семантически и технически это принесет вам немало горя.

введите сюда описание изображения

Давайте начнем с того, что добавим сущность, которая будет хранить ответ на вопрос.

В ней мы будет указано:

  • на какой вопрос мы отвечаем
  • какой вариант ответа мы выбрали
  • кто отвечал

Ответ

@Entity
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @ManyToOne
    public Question question;
    @ManyToOne
    public Variant variant;
    @ManyToOne
    public User user;

    public long getId() {
        return id;
    }

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

    public Question getQuestion() {
        return question;
    }

    public void setQuestion(Question question) {
        this.question = question;
    }

    public Variant getVariant() {
        return variant;
    }

    public void setVariant(Variant variant) {
        this.variant = variant;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

}

Где-то в недрах пользователя можно оставить обратную ссылку (но это не обязательно)
И удалить поле с вариантами (а вот это уже лучше точно сделать)

@Data
@Entity
@Table(name = "users")
public class User {

    // ...

    // это удаляем ↓↓↓
    // @ManyToMany(cascade = { CascadeType.ALL })
    // @JoinTable(
    //         name = "user_variant",
    //         joinColumns = { @JoinColumn(name = "user_id") },
    //         inverseJoinColumns = { @JoinColumn(name = "variant_id") }
    // )
    // private Set<Variants> variants = new HashSet<>();
    // тут удалять перестаем ↑↑↑

    // ...

    @OneToMany(mappedBy = "user")
    private Set<Answer> answers = new HashSet<>();

    // ...

}

Все остальное(помимо этих двух полей) оставляем без изменений.

Также на всякий случай приложу модели Анкеты, вопроса и варианта. К вопросу их, к сожалению, не приложили, поэтому я покажу то, как это выглядело у меня:

Анкета:


@Entity
public class Questionnaires {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String title;
    @OneToMany(mappedBy = "questionnaires")
    private Set<Question> questions = new HashSet<>();

    public long getId() {
        return id;
    }

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

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Set<Question> getQuestions() {
        return questions;
    }

    public void setQuestions(Set<Question> questions) {
        this.questions = questions;
    }
}

Вопрос:

@Entity
public class Question {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String valueQuestion;
    private String choiceType;
    @OneToMany(mappedBy = "question")
    private Set<Variant> variants = new HashSet<>();
    @ManyToOne
    private Questionnaires questionnaires;

    public long getId() {
        return id;
    }

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

    public String getValueQuestion() {
        return valueQuestion;
    }

    public void setValueQuestion(String valueQuestion) {
        this.valueQuestion = valueQuestion;
    }

    public String getChoiceType() {
        return choiceType;
    }

    public void setChoiceType(String choiceType) {
        this.choiceType = choiceType;
    }

    public Set<Variant> getVariants() {
        return variants;
    }

    public void setVariants(Set<Variant> variants) {
        this.variants = variants;
    }

    public Questionnaires getQuestionnaires() {
        return questionnaires;
    }

    public void setQuestionnaires(Questionnaires questionnaires) {
        this.questionnaires = questionnaires;
    }
}

Вариант:

@Entity
public class Variant {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    public String valueVariant;
    @ManyToOne
    public Question question;

    public long getId() {
        return id;
    }

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

    public String getValueVariant() {
        return valueVariant;
    }

    public void setValueVariant(String valueVariant) {
        this.valueVariant = valueVariant;
    }

    public Question getQuestion() {
        return question;
    }

    public void setQuestion(Question question) {
        this.question = question;
    }
}

А что с формой?

Как я и говорил, было бы неплохо иметь класс, описывающую форму анкеты.

Как минимум она должна содержать:

  • объект самой анкеты
  • и было бы неплохо хранить там ответы

Данная модель не обязательно должна быть сущностью связанной с БД.
Но это зависит от того, как вы в дальнейшем будете с этим работать.
Очень даже может быть, что для удобства вам было бы неплохо создать сущность.
Но я этого точно знать не могу.
Моя задача продемонстрировать вам совершенно другое.

Поэтому вот

Форма анкеты:

public class QuestionnairesForm {

    Questionnaires questionnaires;
    List<Answer> answers = new ArrayList<>();

    public Questionnaires getQuestionnaires() {
        return questionnaires;
    }

    public void setQuestionnaires(Questionnaires questionnaires) {
        this.questionnaires = questionnaires;
    }

    public List<Answer> getAnswers() {
        return answers;
    }

    public void setAnswers(List<Answer> answers) {
        this.answers = answers;
    }
}

Что после всего этого будет в контроллере

В методе выводящем анкету:

  • получаем пользователя
  • получаем анкету по id
  • подготовливаем форму, для этого:
    • подготавливаем список ответов
      • находим имеющиеся
      • если нет, то создаем новые с пользователем и вопросом
      • добавляем в список
    • добавляем в форму анкету и подготовленные ответы
@GetMapping("/questionnaire/{id}")
public String listQuestion(@PathVariable Long id, Model model)
{
    // получаем авторизированного пользователя
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String currentPrincipalName = authentication.getName();
    // получаем анкету
    Questionnaires questionnaire = questionnairesRepository.findById(id).get();
    // получаем пользователя
    User user = userRepository.findOneByEmail(currentPrincipalName).get();

    // создаем список для хранения ответов
    List<Answer> answers = new ArrayList<>();
    // получаем список вопросов
    Iterable<Question> questionsFromBD = questionRepository.findByQuestionnaires(questionnaire);
    for (Question question: questionsFromBD) {
        // теоритически пользователь мог уже отвечать на эти вопросы
        // поэтому было бы неплохо найти ответ этого пользователя на данный вопрос в базе данных
        Answer answer = answerRepository.findOneByUserAndQuestion(user, question)
                .orElseGet(() -> { // в противном случае создаем новый объект ответа
                    Answer newAnswer = new Answer();
                    newAnswer.setUser(user); // и указываем пользователя
                    newAnswer.setQuestion(question); // и вопрос
                    return newAnswer;
                });
        answers.add(answer);
    }
    // подготовливаем форму
    QuestionnairesForm questionnairesForm = new QuestionnairesForm();
    questionnairesForm.setQuestionnaires(questionnaire);
    questionnairesForm.setAnswers(answers);
    // и передаем на страницу
    model.addAttribute("questionnairesForm", questionnairesForm);
    return "user_variants";
}

Если нужны репозитории, то у меня они были такими:

QuestionRepository

public interface QuestionRepository extends PagingAndSortingRepository<Question, Long> {
    public List<Question> findByQuestionnaires(Questionnaires questionnaires);
}

AnswerRepository

public interface AnswerRepository extends PagingAndSortingRepository<Answer, Long> {
    Optional<Answer> findOneByUserAndQuestion(User user, Question question);
}

В методе принимающем форму логики не было, поэтому я его оставил практически без изменений.

Единственное, что я сделал - это прокинул в параметры метода результаты формы в качестве модели

@ModelAttribute("questionnairesForm") QuestionnairesForm questionnairesForm

сам метод

@PostMapping("/user/save_answer")
public String processForm(@ModelAttribute("questionnairesForm") QuestionnairesForm questionnairesForm, Model model)
{
    System.out.println(questionnairesForm);
    return "user_variants";
}

Что со всем этим добром в шаблоне делать?

  • привязываем форму к модели, для чего в теге формы указываем атрибут th:model="questionnairesForm"
  • для вывода данных обращаемся к полям questionnairesForm
  • проводим итерацию не по вопросам, а по ответам
  • во время итерации извлекаем, не только текущий элемент, но и метаданные итерации th:each="answer,answerStat : ${questionnairesForm.answers}" в последствии мы можем обратиться к answerStat и извлечь из него индекс текущего элемента
  • в поле указываем на интересующее нас поле, с помощью индекса текущего элемента th:field="*{questionnairesForm.answers[__${answerStat.index}__].variant.id}"
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title></title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form action="#" th:action="@{/user/save_answer}" method="post" th:model="questionnairesForm">
    <input type="hidden" th:value="${questionnairesForm.questionnaires.id}" th:field="*{questionnairesForm.questionnaires.id}"/>
    <input type="hidden" th:value="${questionnairesForm.questionnaires.title}" th:field="*{questionnairesForm.questionnaires.title}"/>
    <div th:each="answer,answerStat : ${questionnairesForm.answers}">
        <h2 th:text="${answer.question.valueQuestion}"></h2>
        <input type="hidden" th:value="${answer.id}" th:field="*{questionnairesForm.answers[__${answerStat.index}__].id}"/>
        <input type="hidden" th:value="${answer.question.valueQuestion}" th:field="*{questionnairesForm.answers[__${answerStat.index}__].question.valueQuestion}"/>
        <input type="hidden" th:value="${answer.question.id}" th:field="*{questionnairesForm.answers[__${answerStat.index}__].question.id}"/>
        <ul>
            <li th:each="variant : ${answer.getQuestion().getVariants()}">
                <input th:type="${variant.question.choiceType}" th:id="${variant.id}"
                       th:field="*{questionnairesForm.answers[__${answerStat.index}__].variant.id}"
                       th:value="${variant.id}">
                <label  th:for="${variant.id}" th:text="${variant.getValueVariant()}"></label>
            </li>
        </ul>
    </div>
    <button type="submit">Complete</button>
</form>
</body>
</html>

На выходе получаем:

введите сюда описание изображения

Если попробуем продебажить метод получания формы то там увидим следующее:

введите сюда описание изображения

Как видите, те данные, что отправили из формы дошли в целости и сохранности до контроллера.

Со всем остальным, я надеюсь разберетесь.

PS:
Обратите внимание, то что при я при создании класса вопроса назвал его Question, а не Questions

→ Ссылка