Javascript ajax post to be handled by Tapestry 5.4 client side

115 Views Asked by At

I am trying to get jquery sortable to work in a Tapestry 5.4 application. The actual sortable table is working, but after a row is moved, I am unable to get the event to make a client side call to actually update the database.

Can anyone help determine what I may not be doing correctly with the $.ajax({}) that is preventing it from creating an event for the Tapestry groovy page to handle? The console.log lines are logging and the positions array has the data desired to completed the database update on the client side.

$(document).ready(function () {
    $('table tbody').sortable({
        update: function (event, ui) {
            $(this).children().each(function (index) {
                if ($(this).attr('data-position') != (index+1)) {
                    $(this).attr('data-position', (index+1)).addClass('updated');
                }
                console.log("tbody update function")
            });

            saveNewPositions();
        }
    });
});

function saveNewPositions() {
    var positions = [];
    $('.updated').each(function () {
        positions.push([$(this).attr('data-index'), $(this).attr('data-position')]);
        $(this).removeClass('updated');

    });
    console.log("saveNewPositions of table update" + positions)

    $.ajax({
        url: "configjquery/tableUpdate",
        method: 'POST',
        dataType: 'text',
        data: {
            update: 1,
            positions: positions
        }, success: function (response) {
            console.log(response);
        }
    });
}

I have tried different tactics for the URL to no avail. When I tried to do the following to create a Tapestry event link and use it for the URL in the ajax function

In the Tapestry Groovy page

String getActionUrl() {
        return resources.createEventLink("tableUpdate").toURI()
    }

Javascript trying to reference method from Tapestry page:

function saveNewPositions() {
    var positions = [];
    $('.updated').each(function () {
        positions.push([$(this).attr('data-index'), $(this).attr('data-position')]);
        $(this).removeClass('updated');

    });
    console.log("saveNewPositions of table update" + positions)

    $.ajax({
        url: "${actionUrl}",
        method: 'POST',
        dataType: 'text',
        data: {
            update: 1,
            positions: positions
        }, success: function (response) {
            console.log(response);
        }
    });
}

It resulted in an error logged to the console: POST http://localhost:8080/cndc/${actionUrl} The actionUrl is not being translated with the getter in the Tapestry page.

I tried creating a var in the $(document).ready(function () for the actionUrl var eventURL = ${actionUrl} and then passing that variable to the saveNewPositions(eventURL) function, but it also resulted in the var not having the actionUrl translated.

1

There are 1 best solutions below

0
user6708591 On

Replacing ${...} expressions works in template files only

Replacing ${...} expressions works in template (tml) files only. That is, if your ${actionUrl} was contained directly in the template file it would get replaced by that property value of the page class. So, putting your JavaScript into a <script> block within the template file would do the trick, alas, at the cost of various disadvantages like (the lack of) reusability, asynchronous loading etc.

Let Tapestry take care of loading the JavaScript

Here is the Tapestry way of solving that problem. Tapestry 5.4+ comes with RequireJS, a framework for loading JavaScript modules asynchronously. You organize your JavaScript logic in modules. Defining a module is often as simple as calling RequireJS's define() function. You can also add entire libraries as modules by contributing to Tapestry's ModuleManager service. Once the modules are defined you can let the page class take care of rendering the code that will instruct the client to load the modules.

The sortable() function is provided by jQuery UI which unlike jQuery does not ship with Tapestry and hence needs to be added as a module. One way of doing that is as follows.

  1. Download and save jQuery UI to src/main/resources/META-INF/assets/jquery-ui/
  2. In your AppModule, define jQuery UI as a module:
@Contribute(ModuleManager.class)
public static void addModules(MappedConfiguration<String, JavaScriptModuleConfiguration> conf,
    @Path("classpath:META-INF/assets/jquery-ui/jquery-ui.min.js") Resource jqueryui) {
  
  conf.add("jqueryui", new JavaScriptModuleConfiguration(jqueryui));

}

With jQuery UI now available as jqueryui, you can now go ahead and reference it in the definition of your own module. Its preliminary definition, in src/main/resources/META-INF/modules/MyItemSorting.js, is as follows:

define(['jquery', 'jqueryui'], function($, qui) {
    
    // The clientId identifies the DOM element to which the sortable
    // widget will be applied. It will be passed by the Tapestry page
    // class.
    return function(clientId) {
        
        $("#" + clientId).sortable(
          // ..
        );
        
    }
});

Now with a rudimentary version of the module defined, you can instruct the page class to render the client code responsible for loading the module.

@InjectComponent
Any itemContainer; // Expects <t:any element="div" t:id="itemContainer"> in the template file.

@Environmental
JavaScriptSupport jsSupport;

@AfterRenderTemplate
void requireModules() {
    jsSupport.require("MyItemSorting").with(itemContainer.getClientId());
}

Having understood how the page class and the module are wired up, you can finish the module definition. Note another module, t5/core/ajax (Tapestry built-in), is used here.

define(['jquery', 'jqueryui', 't5/core/ajax'], function($, qui, ajax) {
    
    return function(clientId) {
    
        $("#" + clientId).sortable({
    
            update: function(event, ui) {
                // (1) Build an array of item identifiers (obtained
                // from the data attributes previously rendered by
                // the template) in the new order 
                var order = [];

                $("#" + clientId).find("div[data-itemid]")
                .each(function(index) {
                    order.push($(this).attr("data-itemid"))
                });
                
                // (2) Send the new order to the page class
                ajax('updateitemorder', {
                    element: null, // null necessary when the page
                                   // (rather than a component)
                                   // is supposed to handle the event
                    
                    data: {order: JSON.stringify(order)},
                    
                    // response.json is the object returned by
                    // the event handler method
                    success: function(response) {
                        console.log(response.json.numUpdated +
                        " items updated on server.");
                    }
                });
            }
            
        });
        
    }
});

I'm aware updating all items is somewhat different from your approach of updating only the ones changed. However, it allows for a more concise answer here. I'm sure you can adapt my example to your needs.

The last thing left to do is make sure the page class properly handles the updateitemorder event called by the ajax() function. Read on.

Use @RequestParameter in the event handler method

Assuming you are using Tapestry 5.4.2 or later, annotate the event handler with @PublishEvent which causes it to become known to the ajax() function.

Adding @RequestParameter to the event handler is crucial as otherwise the data cannot be received from the client.


@Inject
ItemService itemService;

@OnEvent("updateBoatOrder")
@PublishEvent
JSONObject updateItemOrder(@RequestParameter("order") JSONArray order) {

    long numUpdated = itemService.reorder(order);
        
    return new JSONObject("numUpdated", numUpdated);

}

Done! Granted, a bit of a rewrite from where you started (outside of the RequireJS world). But actually not so complicated when you are already in it.

As a closing remark, note that from version 5.5 Tapestry also supports TypeScript. Just add the tapestry-webresources dependency and you are good to go.