1

I would like to trigger Spring MVC controller method from Thymeleaf view without returning anything from the method and without refreshing the page.

I have the following HTML page:

enter image description here

<!-- 'block item' button code-->
<div class="row">
  <form method="get" th:action="@{'/items/block/' + ${item.itemId}}">
      <button type="submit" class="btn btn-warning">block item</button>
  </form>
</div>

The related controller looks as:

@GetMapping("/items/block/{itemId}")
@ResponseStatus(value = HttpStatus.OK)
public void blockItem(@PathVariable String itemId) {
    itemService.blockItem(itemId);
}

The itemService.blockItem(itemId) call just changes the corresponding boolean attribute of the related item object in database.

Currently every time when I trigger this method by pressing block item button I am being redirected to the URL like http://localhost:8080/item/block/7C4A3744375523.

I would like not to do any redirection but just execute the related service method logic. I also need to remove the related item div from the view but this I can do pretty straightforward with JavaScript.
How can I do it? Thanks.

DimaSan
  • 12,264
  • 11
  • 65
  • 75
  • This is a pretty straightforward case for AJAX... use a javascript method to call the url when you click the button (instead of a form). – Metroids May 09 '21 at 14:19
  • You need to use JavaScript to call the URL (for example: https://stackoverflow.com/questions/29775797/fetch-post-json-data), or go with something like HTMX (https://htmx.org/) – Wim Deblauwe May 10 '21 at 11:01

2 Answers2

1

Your button is defined as type submit.

As a consequence, when you click it the form is submitted.

To prevent that default behavior you need several things: the main idea is to define a handler for your form submit event and use Javascript to perform the actual server invocation.

You can do it in different ways, although I think it is preferable to use an event handler to define the desired behavior and separate your HTML code from Javascript as much as possible.

In addition, you seem to be iterating over a list of items.

With those two things in mind consider the following code snippet:

<script>
  // We use the DOMContentLoaded event (https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event)
  // but please, register the form event as you consider appropriate
  document.addEventListener('DOMContentLoaded', () => {
    // Register form submit event handler for every form in your page
    const forms = document.querySelectorAll('form');
    if (forms) {
      forms.forEach(f => {
        let action = f.action;
        f.addEventListener('submit', (event) => {
          // prevent default behavior, i.e., form submission
          event.preventDefault();
          // perform server side request. you can use several options
          // see https://developers.google.com/web/updates/2015/03/introduction-to-fetch, for instance
          // For example, let's use fetch (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
          fetch(action)
            .then((response) => {
              if (!response.ok) {
                throw new Error('Network response was not ok');
              }
              console.log('Request successfully completed');
            })
            .catch((error) => {
              console.error('There has been a problem while contacting server:',      error);
            });
          });
      });
    }
  });
</script>

In fact, you can get rid of the forms. Please, consider a slightly different approach. First, when iterating over your items, generate the following HTML blocks (you can use a data or any custom attribute to store the current processed item id if you prefer to):

<!-- 'block item' button code-->
<div class="row">
  <button type="button" class="btn btn-warning item-button" th:id="${item.itemId}">block item</button>
</div>

Now, in a similar way as described above, register handlers for every item button:

<script th:inline="javascript">
  document.addEventListener('DOMContentLoaded', () => {
    // Register handlers for every button
    const buttons = document.querySelectorAll('.item-button');
    if (buttons) {
      buttons.forEach(b => {
        b.addEventListener('click', (event) => {
          let itemId = b.getAttribute('id');
          let link = /*[[@{/items/block/}]]*/ 'link';
          // Perform server side request
          fetch(link + itemId)
            .then((response) => {
              if (!response.ok) {
                throw new Error('Network response was not ok');
              }
              console.log('Request successfully completed');
            })
            .catch((error) => {
              console.error('There has been a problem while contacting server:', error);
            });
        });
      }
    }
  });
</script>
jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • Thanks - I think this is really interesting approach, but there is something non-compilable in your `` example, did you mean something like below in the Thymeleaf template: ``? Could you please clarify? – DimaSan May 12 '21 at 21:34
  • 1
    You are welcome @DimaSan. Yes, exactly. Sorry, I normally use the [JSLT standard taglib](https://docs.oracle.com/javaee/5/jstl/1.1/docs/tlddocs/c/tld-summary.html) for attribute processing, but yes, it should be equivalent. I updated the answer with thymeleaf specific implementation. Please, sorry again. – jccampanero May 12 '21 at 21:49
  • 1
    @DimaSan I simplified the code to be dependent only on the button `id` attribute. I hope it helps. – jccampanero May 12 '21 at 22:18
  • Thank you for your edit, it calls JS function but because of line `fetch('' + itemId)` - in Chrome console, it failed to load resource, and I got 404 error. I changed it to `fetch('/items/block/' + itemId)`, now it is not complaining but nothing neither happens and controller is not triggered. – DimaSan May 12 '21 at 22:32
  • Sorry, again JSLT. Please, can you try the updated answer? Honestly, I never tried `inlined` javascript with thymeleaf, but I think it should work. Please, see the accepted answer in this [helpful SO question](https://stackoverflow.com/questions/24242554/what-is-the-syntax-to-get-thymeleaf-pagecontext-request-contextpath). – jccampanero May 12 '21 at 22:39
  • In addition, when copying the code from the `form` analogous code I removed the `addEventListener` fragment for the `click` event for the button. This is why it did not work and the browser do nothing. Please, try the updated answer, I think it should work properly now. – jccampanero May 12 '21 at 22:45
1

It's not a Thymeleaf or Spring MVC that causes redirection but submit button in your form tag. You need to overwrite the behaviour of this button to send the form data through Ajax and handle the response by yourself (basically - ignore it) - e.g. using jQuery it could look like:

<!-- 'block item' button code-->
<div class="row">
    <form id="someFormId" method="get" th:action="@{'/items/block/' + ${item.itemId}}">
        <button type="submit" class="btn btn-warning">block item</button>
    </form>
</div>

//...

$('#someFormId').submit(function(e) {
    e.preventDefault();
    $.ajax({
        type: 'GET',
        url: '@{'/items/block/' + ${item.itemId}}',
        data: $(this).serialize(),
        // here you can define some actions for beforeSend, success, etc...
        // for example to just ignore:
        success: function(){}
    });
})

There are many ideas to solve this issue. Please read e.g.

m.antkowicz
  • 13,268
  • 18
  • 37
  • Thank you for your response, but when using your approach the GUI still tries to redirect me to the URL like `http://localhost:8080/items/block/44A13B4122343A` which does not exist. Maybe there is something I need to change on the controller side as well? – DimaSan May 12 '21 at 21:32
  • in my answer I missed `id` attribute of `form` tag - probably this is why it's not working – m.antkowicz May 13 '21 at 05:56
  • 1
    anyway thank you, I believe your answer is very useful as well – DimaSan May 13 '21 at 11:19