Easy ajax operations even without javascript
Description
The Yada Framework support for ajax calls is implemented in yada.ajax.js
.
It is automatically included in production, merged with the other yada javascript files. To use it in development, add:
<script th:if="${@config.developmentEnvironment}" yada:src="@{/yadares/js/yada.ajax.js}"></script>
Ajax is set up via css classes and data attributes in the HTML source. Some data attributes have yada-dialect equivalents which are more readable and easier to use with thymeleaf expressions.
Ajax Links
Calling the backend
To make an ajax request, add the yadaAjax
class:
<a href="/some/endpoint" class="yadaAjax">Click here</a>
When clicking on the anchor, the /some/endpoint
url is called via ajax.
The alternative way to define an ajax link is to use either the yada:ajax
or the data-yadaHref
attributes instead of the yadaAjax
class:
The href attribute should either be empty or "javascript:;" in that case.
<a href="javascript:;" yada:ajax="@{/some/endpoint}">Click here</a>
Sending a Form Group
The ajax link can be part of a Form Group, in which case all data gathered from the forms in the group is sent together with any url parameters:
<a yada:href="@{/some/endpoint(a=1)}" yada:formGroup="myGroup">Click here</a>
<form action="ignored" yada:formGroup="myGroup">
<input name="b" value="2">
</form>
The above example would send a=1&b=2
when clicking on the link.
The link is ignored when the ajax call is started by a form in the group.
Miscellaneous
- timeout
-
You can set a timeout on the ajax call with
data-yadaTimeout="<milliseconds>"
- disable the link
-
The class
yadaAjaxDisabled
disables the link. Can also be used on any other element. - disable the loader
-
The page loader is started by default. To disable it, add the
yadaNoLoader
class. - show an element loader
-
Instead of showing a full page loader, the ajax call can show a small loader inside any page element when the
yada:ajaxElementLoader
attribute is used on the triggering element. The value is a CSS selector that can use the extended syntax. See loaderOption for some more details.
Returning from the backend
Controllers can return a standard thymeleaf view (html/xml), some JSON data or any other MIME type like a PDF or an excel file content. The resulting objects will be passed to any "successHandler" javascript method (see below) when they don’t have other special meaning.
Returning HTML
A very common use case is to return a fragment of the page that originated the request, for example to change a message or to update a table (see Postprocessing for more details):
@RequestMapping("/addRow")
public String addRowAjax() {
// Change something in the model...
// ...then return a fragment of the original page
return "/dashboard :: #table";
}
<a th:href="@{/addRow}" class="yadaAjax" yada:updateOnSuccess="'#table'">
Note that #table
must be in single quotes inside double quotes because it’s a EL literal string.
Returning JSON (using a Map)
A String map can easily be converted to JSON using the standard Spring features:
@RequestMapping(path = "/people", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Map<String, Object> people() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", "Kim");
map.put("age", 44);
return map;
}
Please note the needed @ResponseBody
tag.
The YadaUtil
class contains some utility methods to work with
json as Maps: makeJsonObject()
, setJsonAttribute()
, getJsonAttribute()
etc.
Returning JSON (using a template)
A specific Thymeleaf syntax can result in the production of a json object using the standard Model attribute substitution. The template must have the following form (also in a fragment):
<th:block th:inline="text">["error",
{
"description": "[[${errorDescription}]]"
}
]
</th:block>
This approach has the advantage that the @RequestMapping
method does not need to return a @ResponseBody
so that the json could be conditionally returned (the produces
attribute should
be skipped too in this case).
Returning a notification (with data)
The ajax call can show a notification when going back to the browser. It can also return some key-value data together with the notification. See Notification Modal: Ajax request for details.
Returning any MIME type
By setting the appropriate MediaType, the controller method can return any object, for example a PDF file:
@RequestMapping(path = "/catalog", produces = org.springframework.http.MediaType.APPLICATION_PDF_VALUE)
@ResponseBody
public String downloadCatalog(HttpServletResponse response) {
yadaWebUtil.downloadFile(fileToDownload, false, MediaType.APPLICATION_PDF_VALUE, clientFilename, response);
return null;
}
Yada Commands
The following return values, defined on the YadaViews java class, have special meaning in the context of an ajax call.
Please note that you should NOT use the @ResponseBody
tag in this case.
view name | description |
---|---|
AJAX_SUCCESS |
Do nothing on the browser |
AJAX_REDIRECT |
Perform a redirect on the browser. It uses the Model attributes shown below |
AJAX_RELOAD |
Perform a page reload |
AJAX_CLOSE_MODAL |
Close any modal that might be open |
AJAX_SERVER_ERROR |
Opens a modal with an error message that by default is 'Server Error' unless a Model attribute with a custom message has been added. It uses the Model attributes shown below |
The AJAX_REDIRECT and AJAX_SERVER_ERROR commands use these optional Model attributes:
view name | attribute name | description |
---|---|---|
AJAX_REDIRECT |
AJAX_REDIRECT_URL |
The target absolute url |
AJAX_REDIRECT |
AJAX_REDIRECT_URL_RELATIVE |
The target url relative to the webapp, used if AJAX_REDIRECT_URL is not set. Do not prefix with the language path: it is done automatically. |
AJAX_REDIRECT |
AJAX_REDIRECT_NEWTAB |
Set this attribute to true to open the redirect page in a new tab. Browser popups must be enabled by the user |
AJAX_SERVER_ERROR |
AJAX_SERVER_ERROR_DESCRIPTION |
The custom error message to put in the Model |
examples
Postprocessing
After an ajax call, you usually want to do something on the page: update some div, show a modal, change a javascript variable etc.
The following data-
attributes allow you to perform postprocessing when returning successfully (i.e. with no network errors and no YadaNotify
errors) from the call.
name | value | description |
---|---|---|
|
jQuery selector list (extended syntax) |
replace (default) the selector targets with the result of the ajax call, or replace (default) each selector target with a different part of the result (see below). Other than replacing, also append/prepend operations are possible. |
|
jQuery selector list (extended syntax) |
delete the target elements |
|
comma-separated list of function names, or function body, not both (e.g. "f1,f2" or "f1();f2();" not "f1(),f2") |
call the specified functions or execute the code. The function is called in the context of the DOM element. It also receives the DOM element as a third parameter (see below) |
Yada-dialect variants:
data- HTML attribute | yada-dialect attribute |
---|---|
|
|
|
|
|
|
See below for details.
the difference between using the data- attribute version and the yada: dialect version is that the latter receives an expression that will be evaluated by Thymeleaf. Therefore you can use ${variables} in the value. When the Thymeleaf expression generates a parse error, it is considered a plain string and used as it is: this is different from the th: attributes behavior but it allows using "some plain strings" without quoting them in single quotes. |
Replacing and Deleting
The "jQuery selector list" is a comma-separated list of jQuery selectors, like "#someId, .someClass > a"
.
It is searched in all the document unless a special yada prefix is used (see below).
If the selector list is empty, the target is the element itself.
Do not use an #id as selector. If the selector is an #id, you should ensure that the same id is not present in the returned ajax content or the result might be unexpected. But if you replace something by #id with something that doesn’t have the #id anymore, replacement will work only for the first call. |
Each selector can also have the following special prefixes:
name | description |
---|---|
|
the selector is searched in the children of the current element using |
|
the selector is searched in the parents of the current element using |
|
the selector is searched in the siblings of the current element using |
|
splits the selector at the first space then uses |
|
splits the selector at the first space then uses |
Each selector can also be the argument of a function that alters the behavior from replacing to appending/prepending:
name | description |
---|---|
|
the selected target is replaced with the ajax result (default operation) |
|
the selected target is replaced with the ajax result (alias for $replace()) |
|
the ajax result is inserted as the first child of the selected target |
|
the ajax result is inserted as the last child of the selected target |
The next example prepends the ajax result to the closest div of the clicked button:
<button yada:ajax="@{/someUrl}" yada:updateOnSuccess="$prepend(yadaParents:div)"
Multiple replacement values
If the selector list has many comma-separated selectors and the result contains as many elements
tagged with the class yadaFragment
, then each selector target is given a different yadaFragment
element.
When there are more targets than replacements, replacements are cycled from the start.
When there is a single selector, fragments are ignored and the whole result is used as usual.
The next example shows a @Controller returning two Items, one to be prepended at the button location and another to be appended elsewhere.
<div id="root"> (4)
<h1>There you go</h1>
<!-- itemRoot will end up here -->
</div>
<th:block th:fragment="itemListFragment"> (2)
<div th:each="item : ${itemList}" class="yadaFragment">
<p th:text="${item.text}">Item text here</p>
</div>
</th:block>
<div> (3)
<!-- itemButton will end up here -->
<button yada:ajax="@{/someUrl}" (1)
yada:updateOnSuccess="$prepend(yadaParents:div), $append(#root)">
</button>
</div>
1 | When the user clicks on the button, the controller is called |
2 | The controller returns two items, each tagged with class yadaFragment |
3 | The first item is prepended here by $prepend(yadaParents:div) |
4 | The second item is appended here by $append(#root) |
@RequestMapping("/someUrl")
public String someUrl(Model model) {
...
model.addAttribute("itemList", new Item[] {itemButton, itemRoot});
return "/itemList :: itemListFragment";
}
Calling some Handler
The value of the yadaSuccessHandler attribute can either be a comma-separated list of function names or bodies of a function.
Examples:
yada:successHandler="countItems"
yada:successHandler="countItems, showValue"
yada:successHandler="|$('#${messageId}').addClass('obsolete');|"
yada:successHandler="alert(responseText), alert('hello'), link.reset()"
The success handlers are called in sequence and should have the following signature:
function someHandler(responseText, responseHtml, link) {
- responseText
-
either the unparsed text received from the ajax call, or a json object if the response text is json
- responseHtml
-
the ajax response converted to jQuery html objects
- link
-
the original anchor object (DOM, not jQuery). Could also be a form or anything else, not just a "link".
The link
argument is also the same as the current this
context.
When using function bodies, the above three arguments are valid objects.
If you use both In case |
When |
Modal Dialog
To open a modal returned by an ajax call, see Ajax Modal.
Ajax conditional HTML
When returning from any ajax call the model attribute yadaIsAjaxResponse
has the value yadaIsAjaxResponse
. This can be used to conditionally show
some section or to apply some style only when returning from ajax:
<span th:if="${yadaIsAjaxResponse}">Just returned from Ajax call!</span>
<style>
.yadaIsAjaxResponse { color: red; }
</style>
<div th:classappend="${yadaIsAjaxResponse}">This is red after Ajax</div>
Confirm Dialog
You can show a confirm dialog before the ajax call is made. The user will be shown a text message and an option to confirm or abort the call.
data | tag | description |
---|---|---|
|
|
text to show in the dialog |
|
|
(optional) title of the dialog |
|
|
(optional) text of the confirm button |
|
|
(optional) text of the cancel button |
Ajax Forms
See the Ajax Forms section in the Forms chapter.
Ajax on other elements
Ajax calls can also be made on other HTML elements like buttons and selects by means of the data-yadahref
attribute or the equivalent yada:ajax
dialect.
Furthermore, any HTML element can become a trigger for an ajax call that can asynchronously update a page region when that element
enters the viewport.
Ajax on input fields
An ajax call can be triggered on any <input> field that triggers the "input" event
on change, by just setting the yada:ajax
or data-yadaHref
attribute.
The value of the input field will be sent to the given URL at each keystroke. It is possible
to specify which keystrokes trigger the ajax call by means of the yada:ajaxTriggerKeys
attribute,
that can contain a pipe-separated list of KeyboardEvent.key values.
Example:
<input
yada:ajax="@{/user/setAddress(userId=${user.id})}" (1)
yada:ajaxTriggerKeys="Enter|ArrowRight| |," (2)
yada:updateOnSuccess="yadaParents:.addresses"
yada:ajaxResultFocus (3)
name="address">
1 | URL to call |
2 | optional list of keys that trigger the call: enter, cursor right,space and comma in this example |
3 | on return from the ajax call, after updating the page with the result, if there is an element in the result that has this attribute and is neither disabled nor readonly, it will receive focus (the first one found) |
It works on <input type="radio">
too.
Ajax on checkbox
All <input>, <textarea> and <select> tags can be handled by the new yada.enableAjaxInputs function and the legacy code for them should be removed. Changing a select or a checkbox sumbits the enclosing form: this should be made an option in the new version.
An ajax call can be originated by a state change in a checkbox. The checkbox must NOT be inside a form otherwise the form would be submitted instead.
<input yada:ajax="@{/product/onOff(productId=${product.id})}"
name="enabled" th:checked="${product.enabled}" type="checkbox" />
complete list of ajaxifyable elements. Is the yadaAjax class needed? Examples. showFeedbackIfNeeded
Ajax upload by drag&drop
Any element on page can be turned into a drop target for file upload operations using the following attributes:
name | value | description |
---|---|---|
|
URL |
sends dropped files to the server via ajax |
|
prevents uploading multiple files |
comma-separated title and error text to show in a notification modal when more than one file is dropped |
The current implementation only sends files via ajax |
All other ajax-related functionality can be used, so for example the page can be updated with the result of the
ajax call using yada:updateOnSuccess
, or some function can be called using yada:successHandler
.
A link can also be a drop target, but in case of ajax links any handler or update operation would apply to both clicking and dropping, which probably is not desirable; plain links (non-ajax) always work as expected |
HTML example:
<div class="yadaNoLoader" (1)
yada:updateOnSuccess="yadaParents:.fileDetails" (1)
yada:successHandler="yada.showAjaxFeedback" (1)
yada:dropUpload="@{/uploadGalleryImage(productId=${product.id})}" (2)
yada:singleFileOnly="Too many files,Drop a single file to fill the empty slot" (3)
title="Drop image to upload">
<i class="fas fa-file-upload dropIcon"></i>
</div>
1 | any ajax-related functionality applies |
2 | the endpoint can include request parameters as usual |
3 | in this example ony one file can be dropped |
While the files are being dragged over the drop area, the yadaDropTarget
class is added to the element. This
allows a visual feedback (not provided by default) for example by changing the border or the background color.
The @Controller receives a MultipartFile
or a MultipartFile[]
: dropping a single file will match both signatures
but dropping more than one would only match the latter, so choose accordingly to the use case, the latter being more versatile.
Uploaded files can be handled as when using forms for upload:
@RequestMapping("/uploadProductFiles")
public String uploadProductFiles(Long productId, MultipartFile[] multipartFile, Model model, Locale locale) {
Product product = ...
for (int idx = 0; idx < multipartFile.length; idx++) {
MultipartFile multi = multipartFile[idx];
String clientFilename = multi.getOriginalFilename();
File managedFile = yadaFileManager.uploadFile(multi);
YadaAttachedFile newFile = yadaFileManager.attachNew(true, managedFile, clientFilename, "/productFiles", null, null, null, null);
newFile.setAllTitles(clientFilename);
newFile = yadaAttachedFileDao.save(newFile);
product.addFile(newFile);
}
... save product
return ...
}
Ajax async element load
There are may use cases where it is desirable to load an element of the page only when that element scrolls into view.
For example, deferring the load of a big image or the calculation of a computationally intensive value.
This is achieved using the same yada:ajax
syntax seen above, with the addition of yada:triggerInViewport
: when
any element (even a span) is tagged with yada:triggerInViewport
, it behaves like a clicked anchor when it enters the
viewport (or if it is there already on page load).
When implementing a long list of triggers that get replaced with actual content when entering the viewport, ensure that the triggers actually have some height or they’ll be triggered all at once |
The following example shows an async "like button". On page load the state of the button is unknown so it shows as "not liked". As soon as it gets into view, an ajax call retrieves the real state of the button by querying the DB.
<span yada:triggerInViewport (1)
th:if="${@yadaSecurityUtil.loggedIn}"
class="yadaNoLoader"
yada:ajax="@{/getBookLikeButtonFragment(bookId=${book.id})}" (2)
yada:updateOnSuccess="yadaSiblings:.like"> (3)
</span>
<a class="like" th:fragment="bookLikeButtonFragment" (4)
th:classappend="${isLikedByUser}?liked"> (5)
<i class="bi bi-heart-fill"></i>
</a>
1 | the trigger is a span with no body, but it could be any element, even the button itself (beware of loops!) |
2 | the ajax call sends the book id to the backend; together with the current user id taken from the session (if any) the like state is determined |
3 | when the ajax call returns, the like button is replaced with the result, which is the button itself in this example |
4 | the fragment returned from the Controller is the like button itself (see Java below) |
5 | the "liked" class is added in return from the ajax call when needed |
@RequestMapping("/getBookLikeButtonFragment")
public String getBookLikeButtonFragment(Long bookId, Model model, Locale locale) {
boolean isLikedByUser = false;
Long currentUserProfileId = mySession.getCurrentUserProfileId();
if (currentUserProfileId!=null) {
isLikedByUser = bookDao.isLiked(bookId, currentUserProfileId);
}
model.addAttribute("isLikedByUser", isLikedByUser);
return "/myBooksPage :: bookLikeButtonFragment";
}
Considering that the initial ajax call could be slow and allow users to click on the like button
before it is loaded, it could be desirable to disable it unless it has been loaded via ajax. This
is easily achieved by checking the presence of the yadaIsAjaxResponse
model attribute, that is
inserted at each ajax call. The syntax for adding a second conditional class, in this case yadaAjaxDisabled
,
is a bit more complicated:
<a class="like" ...
th:classappend="|${isLikedByUser==true?'liked':''} ${yadaIsAjaxResponse!=null?'':'yadaAjaxDisabled'}|" (1)
...
1 | yadaAjaxDisabled prevents any ajax call and is already defined in yada.css with a no-drop cursor |
The above example doesn’t take into consideration the action performed when clicking on the like button.
This would be implemented with a plain yada:ajax
call that toggles the like status and returns the
button fragment again:
<a class="like" ...
yada:ajax="@{/user/toggleBookLike(bookId=${book.id},currentLike=${isLikedByUser})}"
yada:updateOnSuccess="">
</a>
Another step would be to take care of "login redirects": when a logged out user clicks on the
like button a login modal would be triggered if the url is protected (as it should) and the
Controller, called after login with a redirect to the original url, wouldn’t know the
real like status from currentLike
. It can be assumed that the user wants to like the item when the like button
is clicked before login (as it is snown as "not liked" by default). For that, there is a request parameter
that is added to the original url and can be checked in the Controller, called yadaAjaxJustLoggedIn
:
@RequestMapping("/toggleBookLike")
public String toggleBookLike(Long bookId, Boolean currentLike, Boolean yadaAjaxJustLoggedIn, Model model, Locale locale) {
Long currentUserProfileId = mySession.getCurrentUserProfileId();
if (currentUserProfileId!=null) {
if (Boolean.TRUE.equals(yadaAjaxJustLoggedIn)) { (1)
bookDao.ensureLiked(bookId, currentUserProfileId); (2)
model.addAttribute("isLikedByUser", true);
} else {
bookDao.toggleLiked(currentLike, bookId, currentUserProfileId); (3)
model.addAttribute("isLikedByUser", !currentLike);
}
}
return "/myBooksPage :: bookLikeButtonFragment";
}
1 | yadaAjaxJustLoggedIn is true when the Controller is called after a login redirect, null otherwise |
2 | force to "like" after a login |
3 | toggle like when no login has just occurred |
Ajax feedback
When returning from an ajax call it might be useful to show some kind of feedback to the user. The yada loader can be used to show that a call is being made but sometimes it is more appropriate to show some feedback when the ajax call has returned, especially when the call is fast and the loader might disappear too quickly.
Such "ajax feedback" can be shown when returning from the ajax call by either using HTML attributes or calling a js method in the success handler.
data | tag | description |
---|---|---|
|
|
enables the ajax feedback |
yada.showAjaxFeedback();
The default feedback shows a green checkmark in the center of the page using the
yadaIcon-ok icon.
This can be styled with the yadaAjaxFeedbackOk
class and
the HTML changed by adding an element with id="yadaAjaxFeedback"
that will be
used in place of the default one.
Ajax method
You can call the low-level yada.ajax() method directly.
yada.ajax(url, data, successHandler, method, timeout, loaderOption, asJson, responseType)
- url
-
the server address to call
- data
-
(optional) string or object to send to the server
- successHandler
-
(optional) javascript method to call after returning from the server (see below)
- method
-
(optional) either "GET" (default) or "POST"
- timeout
-
(optional) milliseconds timeout, null for default (set by the browser)
- loaderOption
-
(optional) controls the display of a loader image (e.g. spinning wheel)
- asJson
-
(optional) true to send the data object as json without splitting the attributes into request parameters
- responseType
-
(optional) the XMLHttpRequest.responseType; use "blob" to download binary data like a pdf file
Everything that applies to the other forms of invocation (opening modals, showing login pages, …) also applies.
URL
The url must point to the controller handling the request. If the javascript code is in an HTML file, the standard thymeleaf [[@{/path}]]
syntax can be used.
If the code is in a js file, the url will have to be passed to the script using some global variable set inside the html file:
window.myUrl = [[@{/path}]]
data
The data object is a standard jQuery.ajax() data object. This means it will be converted using the jQuery conversion rules.
To send some name/value pairs you could therefore use the following code:
var data = {};
data.name = "John";
data.surname = "Doe";
The above would result in two request parameters named "name" and "surname" that can be read on the controller in the usual way:
@RequestMapping("/addUser")
public String addUser(String name, String surname, Model model) {
To send a json object, the asJson
flag must be true:
var data = {name: 'john', surname: 'Doe'};
yada.ajax(url, data, null, "POST", null, false, true);
The controller will then be able to receive a converted Java object:
@RequestMapping("/addUser")
public String addUser(@RequestBody NameSurname data, Model model) {
where NameSurname
is a Java class with the name
and surname
String attributes.
To send a "multipart/form-data" request the data object must be a FormData:
var data = new FormData();
data.append("someBinaryArray", blob);
data.append("someText", text);
yada.ajax(url, data, null, "POST");
This would be equivalent to sending a form via ajax after setting its fields.
The controller should have a MultipartFile
argument for each binary part:
@RequestMapping("/addUser")
public String addUser(MultipartFile someBinaryArray, String someText, Model model) {
More info on binary uploads can be found in File Uploads.
successHandler
The success handler is called when the server returns without errors:
successHandler(responseText, $responseHtml)
- responseText
-
the raw original text returned by the server, or a json object if json was returned
- $responseHtml
-
the original response converted to a div with
jQuery.html()
The successHandler is not invoked if the call returns with a YadaNotify error, unless the executeAnyway
flag is true:
successHandler.executeAnyway=true
If you need to preserve the "this" context of the invoking function, remember to use the "bind" statement:
yada.ajax(urlShowBom, formData, insertBom.bind(this), "POST");
responseType
The response type of an ajax call is set automatically unless specified in this field. A useful value is "blob" for downloading a file on the client computer. See Returning from the backend for an example on how to send a PDF file from the server.
yada.ajax("/catalog", null, null, null, null, null, null, "blob");
loaderOption
By default, when an ajax request is made a "page loader" is shown i.e. the .loader
element is made visible.
To prevent the loader from showing, the loaderOption
must be true
.
Since Yada 0.7.6 a new type of loader can be started instead: the "element loader". This is like the "page loader" but limited to some page elements. For example, when clicking on a button a loader may appear just on that button or on some target div. For the element loader to show, the loaderOption must be either:
-
a string with a css selector
-
a DOM element
-
a JQuery object
It must point to the element or elements that should be covered with a loader.
The HTML of the loader is automatically added and removed.
By default it shows a spinning wheel but can be customised with the
classes yadaElementLoaderOverlay
and yadaElementLoaderIcon
. See yada.css
for the default settings.
The ajax method forces the targeted elements to a "relative" position when "static" or not positioned. This should not affect the layout. If it does, the element loader should target some specifically added element or be removed. |
Utilities
dequeueFunctionCall
The yada.dequeueFunctionCall
function can be used to prevent queuing of ajax calls when only the last call
is useful and a small delay can be tolerated. For example, when sending the value of an input text field at
each keystroke there’s no need to send each keystroke change but only the last value. Using this function, a
keystroke done within the timeout of 200ms will cancel the previous call. Example:
yada.dequeueFunctionCall(document, someFunction);
The parameters are any DOM element used to store a flag, and the function to call.
Class Reference
- yadaAjax
-
Change the standard behavior of the element so that it calls the server via ajax
- yadaAjaxButtonOnly
-
When set on an ajax form, make the form ajax only if the clicked button also has the yadaAjax class. Otherwise the form will be sent with a normal non-ajax request.
- yadaIsAjaxResponse
-
This is the value of the model attribute added when returning to any ajax call. It can for example be used as a class name to apply a different style to page elements when they return from ajax. Example:
<div th:classappend="${yadaIsAjaxResponse}"
TO BE CONTINUED