如何正确将集合从我的表单传递给JS事件处理程序,然后传递给REST控制器?

huangapple go评论138阅读模式
英文:

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.

  1. <!-- I guess this action doesn't make much sense anymore -->
  2. <form action="/save-user" th:object="${user}" method="post">
  3. <input type="hidden" name="id" th:value="${user.id}">
  4. <input type="hidden" name="username"
  5. th:value="${user.username}">
  6. <input type="hidden" name="password"
  7. th:value="${user.password}">
  8. <input type="hidden" name="name" th:value="${user.name}">
  9. <input type="hidden" name="lastName"
  10. th:value="${user.lastName}">
  11. <div class="form-group">
  12. <label for="departments">Department: </label>
  13. <select id="departments" class="form-control"
  14. name="department">
  15. <option th:selected="${user.department == 'accounting'}"
  16. th:value="accounting">Accounting
  17. </option>
  18. <option th:selected="${user.department == 'sales'}"
  19. th:value="sales">Sales
  20. </option>
  21. <option th:selected="${user.department == 'information technology'}"
  22. th:value="'information technology'">IT
  23. </option>
  24. <option th:selected="${user.department == 'human resources'}"
  25. th:value="'human resources'">HR
  26. </option>
  27. <option th:selected="${user.department == 'board of directors'}"
  28. th:value="'board of directors'">Board
  29. </option>
  30. </select>
  31. </div>
  32. <div class="form-group">
  33. <label for="salary">Salary: </label>
  34. <input id="salary" class="form-control" name="salary"
  35. th:value="${user.salary}"
  36. min="100000" aria-describedby="au-salary-help-block"
  37. required/>
  38. <small id="au-salary-help-block"
  39. class="form-text text-muted">100,000+
  40. </small>
  41. </div>
  42. <input type="hidden" name="age" th:value="${user.age}">
  43. <input type="hidden" name="email" th:value="${user.email}">
  44. <input type="hidden" name="enabledByte"
  45. th:value="${user.enabledByte}">
  46. <!-- I guess I should JSON it somehow instead of turning into regular strings -->
  47. <input type="hidden" th:name="authorities"
  48. th:value="${#strings.toString(user.authorities)}"/>
  49. <input class="btn btn-primary d-flex ml-auto" type="submit"
  50. value="Submit">
  51. </form>

Here's my JS:

  1. $(document).ready(function () {
  2. $('form').on('submit', async function (event) {
  3. event.preventDefault();
  4. let user = {
  5. id: $('input[name=id]').val(),
  6. username: $('input[name=username]').val(),
  7. password: $('input[name=password]').val(),
  8. name: $('input[name=name]').val(),
  9. lastName: $('input[name=lastName]').val(),
  10. department: $('select[name=department]').val(),
  11. salary: $('input[name=salary]').val(),
  12. age: $('input[name=age]').val(),
  13. email: $('input[name=email]').val(),
  14. enabledByte: $('input[name=enabledByte]').val(),
  15. authorities: JSON.parse($('input[name=authorities]').val())
  16. };
  17. await fetch('/users', {
  18. method: 'PUT',
  19. headers: {
  20. ...getCsrfHeaders(),
  21. 'Content-Type': 'application/json',
  22. },
  23. body: JSON.stringify(user)
  24. });
  25. });
  26. });
  27. function getCsrfHeaders() {
  28. let csrfToken = $('meta[name="_csrf"]').attr('content');
  29. let csrfHeaderName = $('meta[name="_csrf_header"]').attr('content');
  30. let headers = {};
  31. headers[csrfHeaderName] = csrfToken;
  32. return headers;
  33. }

Here's my REST controller handler:

  1. @PutMapping("/users")
  2. public User updateEmployee(@RequestBody User user) {
  3. service.save(user); // it's JPARepository's regular save()
  4. return user;
  5. }

The User entity:

  1. @Entity
  2. @Table(name = "users")
  3. @Data
  4. @EqualsAndHashCode
  5. public class User implements UserDetails {
  6. @Id
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. @Column
  9. private long id;
  10. @Column(nullable = false, unique = true)
  11. private String username;
  12. @Column(nullable = false)
  13. private String password;
  14. @Column
  15. private String name;
  16. @Column(name = "last_name")
  17. private String lastName;
  18. @Column
  19. private String department;
  20. @Column
  21. private int salary;
  22. @Column
  23. private byte age;
  24. @Column
  25. private String email;
  26. @Column(name = "enabled")
  27. private byte enabledByte;
  28. @ManyToMany
  29. @JoinTable(name = "user_role",
  30. joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id"),
  31. @JoinColumn(name = "username", referencedColumnName = "username")},
  32. inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id"),
  33. @JoinColumn(name = "role", referencedColumnName = "role")})
  34. @EqualsAndHashCode.Exclude
  35. private Set<Role> authorities;
  36. }

The Role entity:

  1. @Entity
  2. @Table(name = "roles")
  3. @Data
  4. @EqualsAndHashCode
  5. public class Role implements GrantedAuthority {
  6. @Id
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. @Column
  9. private long id;
  10. @Column(name = "role", nullable = false, unique = true)
  11. private String authority;
  12. @ManyToMany(mappedBy = "authorities")
  13. @EqualsAndHashCode.Exclude
  14. private Set<User> userList;
  15. }

When I press the submit button, I get this in my console:

  1. 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):

  1. &lt;!-- I guess this action doesn&#39;t make much sense anymore --&gt;
  2. &lt;form action=&quot;/save-user&quot; th:object=&quot;${user}&quot; method=&quot;post&quot;&gt;
  3. &lt;input type=&quot;hidden&quot; name=&quot;id&quot; th:value=&quot;${user.id}&quot;&gt;
  4. &lt;input type=&quot;hidden&quot; name=&quot;username&quot;
  5. th:value=&quot;${user.username}&quot;&gt;
  6. &lt;input type=&quot;hidden&quot; name=&quot;password&quot;
  7. th:value=&quot;${user.password}&quot;&gt;
  8. &lt;input type=&quot;hidden&quot; name=&quot;name&quot; th:value=&quot;${user.name}&quot;&gt;
  9. &lt;input type=&quot;hidden&quot; name=&quot;lastName&quot;
  10. th:value=&quot;${user.lastName}&quot;&gt;
  11. &lt;div class=&quot;form-group&quot;&gt;
  12. &lt;label for=&quot;departments&quot;&gt;Department: &lt;/label&gt;
  13. &lt;select id=&quot;departments&quot; class=&quot;form-control&quot;
  14. name=&quot;department&quot;&gt;
  15. &lt;option th:selected=&quot;${user.department == &#39;accounting&#39;}&quot;
  16. th:value=&quot;accounting&quot;&gt;Accounting
  17. &lt;/option&gt;
  18. &lt;option th:selected=&quot;${user.department == &#39;sales&#39;}&quot;
  19. th:value=&quot;sales&quot;&gt;Sales
  20. &lt;/option&gt;
  21. &lt;option th:selected=&quot;${user.department == &#39;information technology&#39;}&quot;
  22. th:value=&quot;&#39;information technology&#39;&quot;&gt;IT
  23. &lt;/option&gt;
  24. &lt;option th:selected=&quot;${user.department == &#39;human resources&#39;}&quot;
  25. th:value=&quot;&#39;human resources&#39;&quot;&gt;HR
  26. &lt;/option&gt;
  27. &lt;option th:selected=&quot;${user.department == &#39;board of directors&#39;}&quot;
  28. th:value=&quot;&#39;board of directors&#39;&quot;&gt;Board
  29. &lt;/option&gt;
  30. &lt;/select&gt;
  31. &lt;/div&gt;
  32. &lt;div class=&quot;form-group&quot;&gt;
  33. &lt;label for=&quot;salary&quot;&gt;Salary: &lt;/label&gt;
  34. &lt;input id=&quot;salary&quot; class=&quot;form-control&quot; name=&quot;salary&quot;
  35. th:value=&quot;${user.salary}&quot;
  36. min=&quot;100000&quot; aria-describedby=&quot;au-salary-help-block&quot;
  37. required/&gt;
  38. &lt;small id=&quot;au-salary-help-block&quot;
  39. class=&quot;form-text text-muted&quot;&gt;100,000+
  40. &lt;/small&gt;
  41. &lt;/div&gt;
  42. &lt;input type=&quot;hidden&quot; name=&quot;age&quot; th:value=&quot;${user.age}&quot;&gt;
  43. &lt;input type=&quot;hidden&quot; name=&quot;email&quot; th:value=&quot;${user.email}&quot;&gt;
  44. &lt;input type=&quot;hidden&quot; name=&quot;enabledByte&quot;
  45. th:value=&quot;${user.enabledByte}&quot;&gt;
  46. &lt;!-- I guess I should JSON it somehow instead of turning into regular strings --&gt;
  47. &lt;input type=&quot;hidden&quot; th:name=&quot;authorities&quot;
  48. th:value=&quot;${#strings.toString(user.authorities)}&quot;/&gt;
  49. &lt;input class=&quot;btn btn-primary d-flex ml-auto&quot; type=&quot;submit&quot;
  50. value=&quot;Submit&quot;&gt;
  51. &lt;/form&gt;

Here's my JS:

  1. $(document).ready(function () {
  2. $(&#39;form&#39;).on(&#39;submit&#39;, async function (event) {
  3. event.preventDefault();
  4. let user = {
  5. id: $(&#39;input[name=id]&#39;).val(),
  6. username: $(&#39;input[name=username]&#39;).val(),
  7. password: $(&#39;input[name=password]&#39;).val(),
  8. name: $(&#39;input[name=name]&#39;).val(),
  9. lastName: $(&#39;input[name=lastName]&#39;).val(),
  10. department: $(&#39;input[name=department]&#39;).val(),
  11. salary: $(&#39;input[name=salary]&#39;).val(),
  12. age: $(&#39;input[name=age]&#39;).val(),
  13. email: $(&#39;input[name=email]&#39;).val(),
  14. enabledByte: $(&#39;input[name=enabledByte]&#39;).val(),
  15. authorities: $(&#39;input[name=authorities]&#39;).val()
  16. /*
  17. ↑ i tried replacing it with authorities: JSON.stringify($(&#39;input[name=authorities]&#39;).val()), same result
  18. */
  19. };
  20. await fetch(`/users`, {
  21. method: &#39;PUT&#39;,
  22. headers: {
  23. ...getCsrfHeaders(),
  24. &#39;Content-Type&#39;: &#39;application/json&#39;,
  25. },
  26. body: JSON.stringify(user) // tried body : user too
  27. });
  28. });
  29. });
  30. function getCsrfHeaders() {
  31. let csrfToken = $(&#39;meta[name=&quot;_csrf&quot;]&#39;).attr(&#39;content&#39;);
  32. let csrfHeaderName = $(&#39;meta[name=&quot;_csrf_header&quot;]&#39;).attr(&#39;content&#39;);
  33. let headers = {};
  34. headers[csrfHeaderName] = csrfToken;
  35. return headers;
  36. }

Here's my REST controller handler:

  1. // maybe I&#39;ll make it void. i&#39;m not sure i actually want it to return anything
  2. @PutMapping(&quot;/users&quot;)
  3. public User updateEmployee(@RequestBody User user) {
  4. service.save(user); // it&#39;s JPARepository&#39;s regular save()
  5. return user;
  6. }

The User entity:

  1. @Entity
  2. @Table(name = &quot;users&quot;)
  3. @Data
  4. @EqualsAndHashCode
  5. public class User implements UserDetails {
  6. @Id
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. @Column
  9. private long id;
  10. @Column(nullable = false, unique = true)
  11. private String username;
  12. @Column(nullable = false)
  13. private String password;
  14. @Column
  15. private String name;
  16. @Column(name = &quot;last_name&quot;)
  17. private String lastName;
  18. @Column
  19. private String department;
  20. @Column
  21. private int salary;
  22. @Column
  23. private byte age;
  24. @Column
  25. private String email;
  26. @Column(name = &quot;enabled&quot;)
  27. private byte enabledByte;
  28. @ManyToMany
  29. @JoinTable(name = &quot;user_role&quot;,
  30. joinColumns = {@JoinColumn(name = &quot;user_id&quot;, referencedColumnName = &quot;id&quot;),
  31. @JoinColumn(name = &quot;username&quot;, referencedColumnName = &quot;username&quot;)},
  32. inverseJoinColumns = {@JoinColumn(name = &quot;role_id&quot;, referencedColumnName = &quot;id&quot;),
  33. @JoinColumn(name = &quot;role&quot;, referencedColumnName = &quot;role&quot;)})
  34. @EqualsAndHashCode.Exclude
  35. private Set&lt;Role&gt; authorities;

The Role entity:

  1. @Entity
  2. @Table(name = &quot;roles&quot;)
  3. @Data
  4. @EqualsAndHashCode
  5. public class Role implements GrantedAuthority {
  6. @Id
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. @Column
  9. private long id;
  10. @Column(name = &quot;role&quot;, nullable = false, unique = true)
  11. private String authority;
  12. @ManyToMany(mappedBy = &quot;authorities&quot;)
  13. @EqualsAndHashCode.Exclude
  14. private Set&lt;User&gt; userList;

When I press the submit button, I get this in my console

  1. 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&lt;pp.spring_bootstrap.models.Role&gt;` 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

  1. @Override
  2. public void addFormatters(FormatterRegistry registry) {
  3. registry.addFormatter(new Formatter&lt;Set&lt;Role&gt;&gt;() {
  4. @Override
  5. public Set&lt;Role&gt; parse(String text, Locale locale) {
  6. Set&lt;Role&gt; roleSet = new HashSet&lt;&gt;();
  7. String[] roles = text.split(&quot;^\\[|]$|(?&lt;=]),\\s?&quot;);
  8. for (String roleString : roles) {
  9. if (roleString.length() == 0) continue;
  10. String authority =
  11. roleString.substring(roleString.lastIndexOf(&quot;=&quot;) + 2,
  12. roleString.indexOf(&quot;]&quot;) - 1);
  13. roleSet.add(service.getRoleByName(authority));
  14. }
  15. return roleSet;
  16. }
  17. @Override
  18. public String print(Set&lt;Role&gt; object, Locale locale) {
  19. return null;
  20. }
  21. });
  22. }

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)

  1. // inside my config
  2. @Bean
  3. public Function&lt;Set&lt;Role&gt;, String&gt; jsonify() {
  4. return s -&gt; {
  5. StringJoiner sj = new StringJoiner(&quot;, &quot;, &quot;{&quot;, &quot;}&quot;);
  6. for (Role role : s) {
  7. sj.add(String.format(&quot;{ \&quot;id\&quot; : %d, \&quot;authority\&quot; : \&quot;%s\&quot; }&quot;, role.getId(), role.getAuthority()));
  8. }
  9. return sj.toString();
  10. };
  11. }
  1. &lt;input type=&quot;hidden&quot; th:name=&quot;authorities&quot;
  2. th:value=&quot;${@jsonify.apply(user.authorities)}&quot;/&gt;

The method works as expected, though

  1. $(document).ready(function () {
  2. $(&#39;form&#39;).on(&#39;submit&#39;, async function (event) {
  3. /*
  4. ↓ logs:
  5. authorities input: {{ &quot;id&quot; : 1, &quot;authority&quot; : &quot;USER&quot; }}
  6. */
  7. console.log(&#39;authorities input: &#39; +
  8. $(&#39;input[name=authorities]&#39;).val());

UPD2: GPT4 suggested this

  1. authorities: JSON.parse($(&#39;input[name=authorities]&#39;).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

  1. Uncaught (in promise) SyntaxError: Expected property name or &#39;}&#39; in JSON at position 1
  2. at JSON.parse (&lt;anonymous&gt;)
  3. at HTMLFormElement.&lt;anonymous&gt; (script.js:28:31)
  4. at HTMLFormElement.dispatch (jquery.slim.min.js:2:43114)
  5. 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:

  1. // I also added some line breaks, but I doubt it was necessary
  2. @Bean
  3. public Function&lt;Set&lt;Role&gt;, String&gt; jsonify() {
  4. return s -&gt; {
  5. StringJoiner sj = new StringJoiner(&quot;,\n&quot;, &quot;[\n&quot;, &quot;\n]&quot;);
  6. for (Role role : s) {
  7. sj.add(String.format(&quot;{\n\&quot;id\&quot; : %d,\n\&quot;authority\&quot; : \&quot;%s\&quot;\n}&quot;, role.getId(), role.getAuthority()));
  8. }
  9. return sj.toString();
  10. };
  11. }

I also changed, for example, this

  1. username: $(&#39;input[name=username]&#39;).val()

to this (it was silly of me not to do it right away)

  1. username: $(this).find(&#39;input[name=username]&#39;).val()

and – viola – it works now!

And GPT4 also noticed I used

  1. &#39;input[name=department]&#39;

instead of

  1. &#39;select[name=department]&#39;

I fixed that too

答案1

得分: 0

  1. 应该是一个对象数组(尽管它是一个 Collection,而不是一个数组),所以

    new StringJoiner(&quot;, &quot;, &quot;{&quot;, &quot;}&quot;)new StringJoiner(&quot;, &quot;, &quot;[&quot;, &quot;]&quot;)

  2. 应该针对表单的子元素,所以

    username: $(&#39;input[name=username]&#39;).val()username: $(this).find(&#39;input[name=username]&#39;).val() 或者更好的写法是 username: $(this).find(&#39;[name=username]&#39;).val() 等等

  3. department 由一个 &lt;select&gt; 元素表示,所以

    &#39;input[name=department]&#39;&#39;select[name=department]&#39; 或者 &#39;[name=department]&#39;

英文:
  1. It should be an array of objects (even though it's a Collection, not an array) so

new StringJoiner(&quot;, &quot;, &quot;{&quot;, &quot;}&quot;)new StringJoiner(&quot;, &quot;, &quot;[&quot;, &quot;]&quot;)

  1. It should target the form's children so

username: $(&#39;input[name=username]&#39;).val()username: $(this).find(&#39;input[name=username]&#39;).val() or better still username: $(this).find(&#39;[name=username]&#39;).val() and so on

  1. department is represented by a &lt;select&gt; element so

&#39;input[name=department]&#39;&#39;select[name=department]&#39; or &#39;[name=department]&#39;

huangapple
  • 本文由 发表于 2023年4月11日 02:57:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/75979875.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定