Я пытаюсь переписать свои формы таким образом, чтобы не обновлять страницу. Другими словами, я не хочу, чтобы браузер делал какие-либо запросы GET/POST при отправке. jQuery должен помочь мне в этом. Вот моя форма (у меня их несколько):
<!-- I guess this action doesn't make much sense anymore -->
<form action = "/save-user" th:object = "${user}" method = "post">
<input type = "hidden" name = "id" th:value = "${user.id}">
<input type = "hidden" name = "username"
th:value = "${user.username}">
<input type = "hidden" name = "password"
th:value = "${user.password}">
<input type = "hidden" name = "name" th:value = "${user.name}">
<input type = "hidden" name = "lastName"
th:value = "${user.lastName}">
<div class = "form-group">
<label for = "departments">Department: </label>
<select id = "departments" class = "form-control"
name = "department">
<option th:selected = "${user.department == 'accounting'}"
th:value = "accounting">Accounting
</option>
<option th:selected = "${user.department == 'sales'}"
th:value = "sales">Sales
</option>
<option th:selected = "${user.department == 'information technology'}"
th:value = "'information technology'">IT
</option>
<option th:selected = "${user.department == 'human resources'}"
th:value = "'human resources'">HR
</option>
<option th:selected = "${user.department == 'board of directors'}"
th:value = "'board of directors'">Board
</option>
</select>
</div>
<div class = "form-group">
<label for = "salary">Salary: </label>
<input id = "salary" class = "form-control" name = "salary"
th:value = "${user.salary}"
min = "100000" aria-describedby = "au-salary-help-block"
required/>
<small id = "au-salary-help-block"
class = "form-text text-muted">100,000+
</small>
</div>
<input type = "hidden" name = "age" th:value = "${user.age}">
<input type = "hidden" name = "email" th:value = "${user.email}">
<input type = "hidden" name = "enabledByte"
th:value = "${user.enabledByte}">
<!-- I guess I should JSON it somehow instead of turning into regular strings -->
<input type = "hidden" th:name = "authorities"
th:value = "${#strings.toString(user.authorities)}"/>
<input class = "btn btn-primary d-flex ml-auto" type = "submit"
value = "Submit">
</form>
Вот мой JS:
$(document).ready(function () {
$('form').on('submit', async function (event) {
event.preventDefault();
let user = {
id: $('input[name=id]').val(),
username: $('input[name=username]').val(),
password: $('input[name=password]').val(),
name: $('input[name=name]').val(),
lastName: $('input[name=lastName]').val(),
department: $('input[name=department]').val(),
salary: $('input[name=salary]').val(),
age: $('input[name=age]').val(),
email: $('input[name=email]').val(),
enabledByte: $('input[name=enabledByte]').val(),
authorities: $('input[name=authorities]').val()
/*
↑ i tried replacing it with authorities: JSON.stringify($('input[name=authorities]').val()), same result
*/
};
await fetch(`/users`, {
method: 'PUT',
headers: {
...getCsrfHeaders(),
'Content-Type': 'application/json',
},
body: JSON.stringify(user) // tried body : user too
});
});
});
function getCsrfHeaders() {
let csrfToken = $('meta[name = "_csrf"]').attr('content');
let csrfHeaderName = $('meta[name = "_csrf_header"]').attr('content');
let headers = {};
headers[csrfHeaderName] = csrfToken;
return headers;
}
Вот мой обработчик контроллера REST:
// maybe I'll make it void. i'm not sure i actually want it to return anything
@PutMapping("/users")
public User updateEmployee(@RequestBody User user) {
service.save(user); // it's JPARepository's regular save()
return user;
}
Сущность User
:
@Entity
@Table(name = "users")
@Data
@EqualsAndHashCode
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column
private String name;
@Column(name = "last_name")
private String lastName;
@Column
private String department;
@Column
private int salary;
@Column
private byte age;
@Column
private String email;
@Column(name = "enabled")
private byte enabledByte;
@ManyToMany
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id"),
@JoinColumn(name = "username", referencedColumnName = "username")},
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id"),
@JoinColumn(name = "role", referencedColumnName = "role")})
@EqualsAndHashCode.Exclude
private Set<Role> authorities;
Сущность Role
:
@Entity
@Table(name = "roles")
@Data
@EqualsAndHashCode
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private long id;
@Column(name = "role", nullable = false, unique = true)
private String authority;
@ManyToMany(mappedBy = "authorities")
@EqualsAndHashCode.Exclude
private Set<User> userList;
Когда я нажимаю кнопку отправки, я получаю это в своей консоли
WARN 18252 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.HashSet<pp.spring_bootstrap.models.Role>` from String value (token `JsonToken.VALUE_STRING`)]
Кажется, я должен каким-то образом передать JSON-представление этого Collection
, а не просто String
. В моем предыдущем проекте без использования jQuery String
была успешно десериализована с помощью моего пользовательского Formatter
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new Formatter<Set<Role>>() {
@Override
public Set<Role> parse(String text, Locale locale) {
Set<Role> roleSet = new HashSet<>();
String[] roles = text.split("^\\[|]$|(?<=]),\\s?");
for (String roleString : roles) {
if (roleString.length() == 0) continue;
String authority =
roleString.substring(roleString.lastIndexOf(" = ") + 2,
roleString.indexOf("]") - 1);
roleSet.add(service.getRoleByName(authority));
}
return roleSet;
}
@Override
public String print(Set<Role> object, Locale locale) {
return null;
}
});
}
Я погуглил, и оказалось, что у Thymeleaf нет никакого метода toJson()
. Я имею в виду, что могу написать свои собственные методы, но я все равно не знаю, как их использовать в шаблонах Thymeleaf. К тому же это может быть не самое оптимальное решение
Это загрузочный проект, поэтому у меня есть библиотека привязки данных Джексона.
Как мне правильно передать коллекцию из моей формы в обработчик событий JS, а затем в контроллер REST?
Я изучил несколько похожих вопросов, предложенных StackOverflow. Они не кажутся релевантными (например, они связаны с разными языками программирования, такими как C# или PHP).
UPD: только что попробовал. Жаль, что это тоже не сработало! (сообщение об ошибке такое же)
// inside my config
@Bean
public Function<Set<Role>, String> jsonify() {
return s -> {
StringJoiner sj = new StringJoiner(", ", "{", "}");
for (Role role : s) {
sj.add(String.format("{ \"id\" : %d, \"authority\" : \"%s\" }", role.getId(), role.getAuthority()));
}
return sj.toString();
};
}
<input type = "hidden" th:name = "authorities"
th:value = "${@jsonify.apply(user.authorities)}"/>
Однако метод работает так, как ожидалось.
$(document).ready(function () {
$('form').on('submit', async function (event) {
/*
↓ logs:
authorities input: {{ "id" : 1, "authority" : "USER" }}
*/
console.info('authorities input: ' +
$('input[name=authorities]').val());
UPD2: GPT4 предложил это
authorities: JSON.parse($('input[name=authorities]').val())
а сейчас вообще странно. База данных осталась неизменной, НО! консоль IDE теперь не имеет ошибок и вообще не упоминает запрос PUT (он был там при предыдущих попытках)! Кроме того, в журнале браузера есть это сообщение
Uncaught (in promise) SyntaxError: Expected property name or '}' in JSON at position 1
at JSON.parse (<anonymous>)
at HTMLFormElement.<anonymous> (script.js:28:31)
at HTMLFormElement.dispatch (jquery.slim.min.js:2:43114)
at v.handle (jquery.slim.min.js:2:41098)
Я не знаю, что это значит!
UPD3: GPT4 шустрый. Во всяком случае, умнее меня. Это было абсолютно правильно. Причина, по которой это не сработало в UPD2, заключалась в том, что я проигнорировал еще одну вещь, которую он сказал:
Поле полномочий следует отправлять в виде массива объектов, а не строки.
Это означает, что я должен был использовать квадратные, а не фигурные скобки в качестве префикса и суффикса StringJoiner
:
// I also added some line breaks, but I doubt it was necessary
@Bean
public Function<Set<Role>, String> jsonify() {
return s -> {
StringJoiner sj = new StringJoiner(",\n", "[\n", "\n]");
for (Role role : s) {
sj.add(String.format("{\n\"id\" : %d,\n\"authority\" : \"%s\"\n}", role.getId(), role.getAuthority()));
}
return sj.toString();
};
}
Я также изменил, например, это
username: $('input[name=username]').val()
к этому (глупо было с моей стороны не сделать это сразу)
username: $(this).find('input[name=username]').val()
и – альт – теперь это работает!
И GPT4 также заметил, что я использовал
'input[name=department]'
вместо
'select[name=department]'
я тоже это исправил
Collection
, а не массив), поэтомуnew StringJoiner(", ", "{", "}")
→ new StringJoiner(", ", "[", "]")
username: $('input[name=username]').val()
→ username: $(this).find('input[name=username]').val()
или еще лучше username: $(this).find('[name=username]').val()
и так далее
department
представлен элементом <select>
, поэтому'input[name=department]'
→ 'select[name=department]'
или '[name=department]'