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

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

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

    &lt;!-- I guess this action doesn&#39;t make much sense anymore --&gt;
    &lt;form action=&quot;/save-user&quot; th:object=&quot;${user}&quot; method=&quot;post&quot;&gt;
         &lt;input type=&quot;hidden&quot; name=&quot;id&quot; th:value=&quot;${user.id}&quot;&gt;
    
         &lt;input type=&quot;hidden&quot; name=&quot;username&quot;
              th:value=&quot;${user.username}&quot;&gt;
    
         &lt;input type=&quot;hidden&quot; name=&quot;password&quot;
              th:value=&quot;${user.password}&quot;&gt;
    
         &lt;input type=&quot;hidden&quot; name=&quot;name&quot; th:value=&quot;${user.name}&quot;&gt;
    
         &lt;input type=&quot;hidden&quot; name=&quot;lastName&quot;
              th:value=&quot;${user.lastName}&quot;&gt;
    
         &lt;div class=&quot;form-group&quot;&gt;
              &lt;label for=&quot;departments&quot;&gt;Department: &lt;/label&gt;
              &lt;select id=&quot;departments&quot; class=&quot;form-control&quot;
                       name=&quot;department&quot;&gt;
                  &lt;option th:selected=&quot;${user.department == &#39;accounting&#39;}&quot;
                          th:value=&quot;accounting&quot;&gt;Accounting
                  &lt;/option&gt;
                  &lt;option th:selected=&quot;${user.department == &#39;sales&#39;}&quot;
                          th:value=&quot;sales&quot;&gt;Sales
                  &lt;/option&gt;
                  &lt;option th:selected=&quot;${user.department == &#39;information technology&#39;}&quot;
                          th:value=&quot;&#39;information technology&#39;&quot;&gt;IT
                  &lt;/option&gt;
                  &lt;option th:selected=&quot;${user.department == &#39;human resources&#39;}&quot;
                          th:value=&quot;&#39;human resources&#39;&quot;&gt;HR
                  &lt;/option&gt;
                  &lt;option th:selected=&quot;${user.department == &#39;board of directors&#39;}&quot;
                          th:value=&quot;&#39;board of directors&#39;&quot;&gt;Board
                  &lt;/option&gt;
              &lt;/select&gt;
          &lt;/div&gt;
    
          &lt;div class=&quot;form-group&quot;&gt;
              &lt;label for=&quot;salary&quot;&gt;Salary: &lt;/label&gt;
              &lt;input id=&quot;salary&quot; class=&quot;form-control&quot; name=&quot;salary&quot;
                     th:value=&quot;${user.salary}&quot;
                     min=&quot;100000&quot; aria-describedby=&quot;au-salary-help-block&quot;
                     required/&gt;
              &lt;small id=&quot;au-salary-help-block&quot;
                     class=&quot;form-text text-muted&quot;&gt;100,000+
              &lt;/small&gt;
          &lt;/div&gt;
    
          &lt;input type=&quot;hidden&quot; name=&quot;age&quot; th:value=&quot;${user.age}&quot;&gt;
    
          &lt;input type=&quot;hidden&quot; name=&quot;email&quot; th:value=&quot;${user.email}&quot;&gt;
    
          &lt;input type=&quot;hidden&quot; name=&quot;enabledByte&quot;
                 th:value=&quot;${user.enabledByte}&quot;&gt;
          
          &lt;!-- I guess I should JSON it somehow instead of turning into regular strings --&gt;
          &lt;input type=&quot;hidden&quot; th:name=&quot;authorities&quot;
                 th:value=&quot;${#strings.toString(user.authorities)}&quot;/&gt;
    
          &lt;input class=&quot;btn btn-primary d-flex ml-auto&quot; type=&quot;submit&quot;
                 value=&quot;Submit&quot;&gt;
      &lt;/form&gt;

Here's my JS:

$(document).ready(function () {
    $(&#39;form&#39;).on(&#39;submit&#39;, async function (event) {
        event.preventDefault();

        let user = {
            id: $(&#39;input[name=id]&#39;).val(),
            username: $(&#39;input[name=username]&#39;).val(),
            password: $(&#39;input[name=password]&#39;).val(),
            name: $(&#39;input[name=name]&#39;).val(),
            lastName: $(&#39;input[name=lastName]&#39;).val(),
            department: $(&#39;input[name=department]&#39;).val(),
            salary: $(&#39;input[name=salary]&#39;).val(),
            age: $(&#39;input[name=age]&#39;).val(),
            email: $(&#39;input[name=email]&#39;).val(),
            enabledByte: $(&#39;input[name=enabledByte]&#39;).val(),
            authorities: $(&#39;input[name=authorities]&#39;).val()

            /*
            ↑ i tried replacing it with authorities: JSON.stringify($(&#39;input[name=authorities]&#39;).val()), same result
            */
        };

        await fetch(`/users`, {
            method: &#39;PUT&#39;,
            headers: {
                ...getCsrfHeaders(),
                &#39;Content-Type&#39;: &#39;application/json&#39;,
            },
            body: JSON.stringify(user) // tried body : user too
        });

    });
});

function getCsrfHeaders() {
    let csrfToken = $(&#39;meta[name=&quot;_csrf&quot;]&#39;).attr(&#39;content&#39;);
    let csrfHeaderName = $(&#39;meta[name=&quot;_csrf_header&quot;]&#39;).attr(&#39;content&#39;);

    let headers = {};
    headers[csrfHeaderName] = csrfToken;
    return headers;
}

Here's my REST controller handler:

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

The User entity:

@Entity
@Table(name = &quot;users&quot;)
@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 = &quot;last_name&quot;)
    private String lastName;
    @Column
    private String department;
    @Column
    private int salary;
    @Column
    private byte age;
    @Column
    private String email;
    @Column(name = &quot;enabled&quot;)
    private byte enabledByte;
    @ManyToMany
    @JoinTable(name = &quot;user_role&quot;,
            joinColumns = {@JoinColumn(name = &quot;user_id&quot;, referencedColumnName = &quot;id&quot;),
                    @JoinColumn(name = &quot;username&quot;, referencedColumnName = &quot;username&quot;)},
            inverseJoinColumns = {@JoinColumn(name = &quot;role_id&quot;, referencedColumnName = &quot;id&quot;),
                    @JoinColumn(name = &quot;role&quot;, referencedColumnName = &quot;role&quot;)})
    @EqualsAndHashCode.Exclude
    private Set&lt;Role&gt; authorities;

The Role entity:

@Entity
@Table(name = &quot;roles&quot;)
@Data
@EqualsAndHashCode
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private long id;
    @Column(name = &quot;role&quot;, nullable = false, unique = true)
    private String authority;
    @ManyToMany(mappedBy = &quot;authorities&quot;)
    @EqualsAndHashCode.Exclude
    private Set&lt;User&gt; 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&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

@Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new Formatter&lt;Set&lt;Role&gt;&gt;() {
            @Override
            public Set&lt;Role&gt; parse(String text, Locale locale) {
                Set&lt;Role&gt; roleSet = new HashSet&lt;&gt;();
                String[] roles = text.split(&quot;^\\[|]$|(?&lt;=]),\\s?&quot;);
                for (String roleString : roles) {
                    if (roleString.length() == 0) continue;
                    String authority =
                            roleString.substring(roleString.lastIndexOf(&quot;=&quot;) + 2,
                                    roleString.indexOf(&quot;]&quot;) - 1);
                    roleSet.add(service.getRoleByName(authority));
                }
                return roleSet;
            }

            @Override
            public String print(Set&lt;Role&gt; 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&lt;Set&lt;Role&gt;, String&gt; jsonify() {
        return s -&gt; {
            StringJoiner sj = new StringJoiner(&quot;, &quot;, &quot;{&quot;, &quot;}&quot;);
            for (Role role : s) {
                sj.add(String.format(&quot;{ \&quot;id\&quot; : %d, \&quot;authority\&quot; : \&quot;%s\&quot; }&quot;, role.getId(), role.getAuthority()));
            }
            return sj.toString();
        };
    }
    &lt;input type=&quot;hidden&quot; th:name=&quot;authorities&quot; 
           th:value=&quot;${@jsonify.apply(user.authorities)}&quot;/&gt;

The method works as expected, though

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

UPD2: GPT4 suggested this

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

Uncaught (in promise) SyntaxError: Expected property name or &#39;}&#39; in JSON at position 1
at JSON.parse (&lt;anonymous&gt;)
at HTMLFormElement.&lt;anonymous&gt; (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&lt;Set&lt;Role&gt;, String&gt; jsonify() {
        return s -&gt; {
            StringJoiner sj = new StringJoiner(&quot;,\n&quot;, &quot;[\n&quot;, &quot;\n]&quot;);
            for (Role role : s) {
                sj.add(String.format(&quot;{\n\&quot;id\&quot; : %d,\n\&quot;authority\&quot; : \&quot;%s\&quot;\n}&quot;, role.getId(), role.getAuthority()));
            }
            return sj.toString();
        };
    }

I also changed, for example, this

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

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

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

and – viola – it works now!

And GPT4 also noticed I used

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

instead of

&#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:

确定