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"

Что необходимо исправить что бы для каждого вопроса выбиралась одна радиокнопка и при заполнении варианты ответов заносились в поле вариантов юзера?
Ответы (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



