英文:
How do I correctly pass a Collection from my form to a JS event handler and then to a REST controller?
问题
I am trying to rewrite my forms in a way that doesn't involve any page refreshing. In other words, I don't want the browser to make any GET/POST requests on submit. jQuery should help me with that.
<!-- 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>
Here's my 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: $('select[name=department]').val(),
salary: $('input[name=salary]').val(),
age: $('input[name=age]').val(),
email: $('input[name=email]').val(),
enabledByte: $('input[name=enabledByte]').val(),
authorities: JSON.parse($('input[name=authorities]').val())
};
await fetch('/users', {
method: 'PUT',
headers: {
...getCsrfHeaders(),
'Content-Type': 'application/json',
},
body: JSON.stringify(user)
});
});
});
function getCsrfHeaders() {
let csrfToken = $('meta[name="_csrf"]').attr('content');
let csrfHeaderName = $('meta[name="_csrf_header"]').attr('content');
let headers = {};
headers[csrfHeaderName] = csrfToken;
return headers;
}
Here's my REST controller handler:
@PutMapping("/users")
public User updateEmployee(@RequestBody User user) {
service.save(user); // it's JPARepository's regular save()
return user;
}
The User
entity:
@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;
}
The Role
entity:
@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;
}
When I press the submit button, I get this in my console:
authorities input: [{ "id" : 1, "authority" : "USER" }]
UPD3: GPT-4 is correct. You should send the authorities
field as an array of objects, not a string. The use of square brackets in your StringJoiner
for JSON representation is essential. Also, make sure to select elements within the form correctly using $(this).find('...')
to avoid any issues.
英文:
I am trying to rewrite my forms in a way that doesn't involve any page refreshing. In other words, I don't want the browser to make any GET/POST requests on submit. jQuery should help me with that.
Here's my form (I have a few of them):
<!-- 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>
Here's my 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;
}
Here's my REST controller handler:
// 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;
}
The User
entity:
@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;
The Role
entity:
@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;
When I press the submit button, I get this in my console
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`)]
It seems I should somehow pass a JSON representation of that Collection
, not just a String
. In my previous project, without the use of jQuery, the String
was successfully deserialized with my custom 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;
}
});
}
I googled, and it appears Thymeleaf doesn't have any toJson()
method. I mean I can write my own methods, but I don't know how to use them in Thymeleaf templates anyway. Besides, it may not be the most optimal solution
It's a Boot project so I have that Jackson databind library
How do I correctly pass a Collection from my form to a JS event handler and then to a REST controller?
I examined multiple similar questions suggested by StackOverflow. They don't appear relevant (for example, they involve different programming languages, such as C# or PHP)
UPD: I tried this just now. It's a pitty it didn't work too! (the error message is the same)
// 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)}"/>
The method works as expected, though
$(document).ready(function () {
$('form').on('submit', async function (event) {
/*
↓ logs:
authorities input: {{ "id" : 1, "authority" : "USER" }}
*/
console.log('authorities input: ' +
$('input[name=authorities]').val());
UPD2: GPT4 suggested this
authorities: JSON.parse($('input[name=authorities]').val())
and now it's really weird. The database is still unchanged, BUT! the IDE console now has no errors and no mentioning of a PUT request at all (it was there on previous attempts)! Additionally, the browser log has this message
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)
I don't know what it means!
UPD3: GPT4 is smart. Smarter than me, anyways. It was absolutely right. The reason it didn't work in UPD2 was the fact that I ignored another thing it said:
> The authorities field should be sent as an array of objects rather than a string.
It means I was supposed to use square brackets, not curly brackets, as my StringJoiner
prefix and suffix:
// 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();
};
}
I also changed, for example, this
username: $('input[name=username]').val()
to this (it was silly of me not to do it right away)
username: $(this).find('input[name=username]').val()
and – viola – it works now!
And GPT4 also noticed I used
'input[name=department]'
instead of
'select[name=department]'
I fixed that too
答案1
得分: 0
-
应该是一个对象数组(尽管它是一个
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]'
英文:
- It should be an array of objects (even though it's a
Collection
, not an array) so
new StringJoiner(", ", "{", "}")
→ new StringJoiner(", ", "[", "]")
- It should target the form's children so
username: $('input[name=username]').val()
→ username: $(this).find('input[name=username]').val()
or better still username: $(this).find('[name=username]').val()
and so on
department
is represented by a<select>
element so
'input[name=department]'
→ 'select[name=department]'
or '[name=department]'
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论