I'm trying to rewrite my form in a way that doesn't involve any page refreshes. In other words, I don't want the browser to make any GET/POST requests on submit. jQuery should be able to help me solve this problem. Here is my table (I have a few):
<!-- 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>
This is 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; }
This is 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; }
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;
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 see this in the 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 the JSON representation of the Collection
instead of just the String
. In my previous project, without using jQuery, String
was successfully deserialized using 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 it and it seems 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. Additionally, this may not be the optimal solution
This is a Boot project, so I have the Jackson data binding library
How do I properly pass a Collection from my form to a JS event handler and then to a REST controller?
I checked multiple similar questions suggested by StackOverflow. They don't look related (e.g. they involve different programming languages like C# or PHP)
UPD: I just tried this. Unfortunately it didn’t work either! (Same error message)
// 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)}"/>
However, the method works as expected
$(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 recommends this
authorities: JSON.parse($('input[name=authorities]').val())
It’s really weird now. The database still hasn't changed, though! The IDE console now has no errors and no mention of the PUT request at all (which was also present in the previous attempt)! Also, 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 do not know what it meant!
UPD3: GPT4 is very smart. Smarter than me anyway. This is absolutely true. The reason it doesn't work in UPD2 is that I ignored another thing it said:
Permissions fields should be sent as an array of objects rather than as strings.
This means I should use square brackets, not braces, 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 it, such as this
username: $('input[name=username]').val()
To this (I was stupid for not doing this right away)
username: $(this).find('input[name=username]').val()
And - viola - it's now available!
GPT4 also noticed that I used
'input[name=department]'
instead of
'select[name=department]'
I also solved this problem
Collection
, not an array), sonew StringJoiner(", ", "{", "}")
→new StringJoiner(", ", "[", "]")
p>Username: $('input[name=username]').val()
→Username: $(this).find('input[name=username]') . val()
or betterUsername: $(this).find('[name=username]').val()
etc.department
is represented by theelement, so
'input[name=department]'
→'select[name=department]'
or'[name=department]'