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.

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 loader is started by default. To disable it, add the yadaNoLoader class.

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.

Table 1. data- attributes for ajax postprocessing
name value description

data-yadaUpdateOnSuccess

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.

data-yadaDeleteOnSuccess

jQuery selector list (extended syntax)

delete the target elements

data-yadaSuccessHandler

comma-separated list of function names or function body

call the specified functions or execute the code

Yada-dialect variants:

data- HTML attribute yada-dialect attribute

data-yadaUpdateOnSuccess

yada:updateOnSuccess

data-yadaDeleteOnSuccess

yada:deleteOnSuccess

data-yadaSuccessHandler

yada:successHandler

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.

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.

Each selector can also have the following special prefixes:

name description

yadaFind:

the selector is searched in the children of the current element using $.find()

yadaParents:

the selector is searched in the parents of the current element using $.closest()

yadaSiblings:

the selector is searched in the siblings of the current element using $.siblings()

yadaClosestFind:

splits the selector at the first space then uses $.closest() with the first part and $.find() with the second

yadaSiblingsFind:

splits the selector at the first space then uses $.siblings() with the first part and $.find() with the second

Each selector can also be the argument of a function that alters the behavior from replacing to appending/prepending:

name description

$replace()

the selected target is replaced with the ajax result (default operation)

$replaceWith()

the selected target is replaced with the ajax result (alias for $replace())

$prepend()

the ajax result is inserted as the first child of the selected target

$append()

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 yada:updateOnSuccess and yada:successHandler, the handlers will be called after the page has been modified and the responseHtml argument would point to the new page content. The this context would be the original element, that may no longer be on page if replaced.

In case yada:updateOnSuccess worked on multiple elements, the responseHtml argument would be an array of all sections inserted in page.

When yada:successHandler is used alone, the responseHtml is added to a parent <div> so that css selection (like $.find()) can match root nodes. When using both yada:updateOnSuccess and yada:successHandler, the responseHtml argument does not have the added <div> (stripped by yada:updateOnSuccess for technical reasons) and it stays whatever was returned by the ajax call: if you need to match a root object, the selector won’t work. You may consider using yada.findFromParent('.parent', '.child', responseHtml) in all handlers to cater for both cases, instead of the classic responseHtml.find('.parent .child') or $('.parent .child', responseHtml) that may fail.

Modal Dialog

To open a modal returned by an ajax call, see Ajax Modal.

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.

Table 2. data- attributes and tags for Confirm Dialog
data tag description

data-yadaConfirm

yada:confirm

text to show in the dialog

data-yadaTitle

yada:title

(optional) title of the dialog

data-yadaOkButton

yada:okButton

(optional) text of the confirm button

data-yadaCancelButton

yada:cancelButton

(optional) text of the cancel button

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>

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

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 method

You can call the low-level yada.ajax() method directly.

yada.ajax(url, data, successHandler, method, timeout, hideLoader, 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)

hideLoader

(optional) true for not showing the spinning loader (shown by default)

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");

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