SAP Tutorial: Complete CAP Java 13

Brian Heise
11 min readSep 24, 2021

--

beforeOpen events for dialogs and creating input forms with the oData V4 Model

Photo by John Schnobrich on Unsplash

Contents

Here we are at Part 13 of my Complete CAP Java tutorial series, where I’m showing you how to build an application using SAP’s CAP for Java framework by showing you step by step how to recreate the sample CAP application that SAP has provided, with some tweaks and improvements along the way. This application is a bookshop app, and we’ve currently been working on a page for customers to browse the available books. As always, the code for this tutorial can be found in this git repo.

Last week we added a custom SAPUI5 button and dialog to our books table so that we will be able to add a review from the List Report page. However, we currently just have an empty dialog, as shown below:

Our empty Add Review dialog

This week we’re going to set up an beforeOpen event handler to bind a particular book to the dialog and how to work around a common difficulty with using the oData V4 model to handle creating single entities without a table. Let’s get started!

Step 1: Setting Up A beforeOpen Event Handler

Remember that for performance purposes we set up our dialog to have only a single instance for the entire page. However, this means that we’ll need some custom logic to run before the dialog opens to bind the book of the row we clicked to the dialog so we can leverage SAPUI5’s oData V4 model to create the new Review. The first step in doing that is to define an onBeforeOpen event handler in our AddReviewDialogHandler file. For now, the only content of the function is logging a message so we can confirm that the function is running.

Defining beforeOpenDialog

Ordinarily we would designate this function as the onBeforeOpen event handler in the XML fragment file, but in this case we can’t do that because we need to pass some custom information when we open it that only the button knows about. Therefore, we’ll programmatically assign this function through our AddReviewButtonHandler file.

We do this by first importing our AddReviewButtonHandler as shown below:

Importing the dialog handler

After that, we use the attachBeforeOpen method of the dialog and pass the function to run on opening, as shown below.

Attaching the beforeOpen event handler

Try running the app now and we can confirm that the function runs as expected:

Confirming that the function ran

Of course, if you try opening this a few times, you’ll notice that each time we open the dialog, the function runs an increasing number of times:

I imagine you already know the issue, but basically the problem is every time we open the dialog, we attach the event handler again; it will run once for each time we attach it, ever increasing with each click. This is no good of course! But, because we potentially need new input data if the user clicks a button on a different row, we can’t simply check to see if the function already exists before attaching it; instead, we need to detach it once the dialog has opened using the detachBeforeOpen method, as shown below:

Detaching the beforeOpen handler after opening

Now, if you go back to the UI and click the button, you’ll see that the function only runs once per click, no matter how many times you click it.

Step 2: Passing Parameters to the Before Open Handler

Now we have our new event handler, but it won’t do us any good if we can’t pass it the binding information for the book of the row we clicked on. Let’s learn how to do that now.

First of all, there are two pieces of data we need to pass to our function — the ID of the dialog itself (which we will need to access the dialog and its contents from within our dialog handler) and the binding context path, which we will use to indicate to SAPUI5 which book we want to attach the review to. Let’s get started.

Recall that we already have the dialog’s id, but we need to refactor a bit so that we actually save the ID so we can use it later.

We generate the ID, then promptly lose it
Refactor to cache the ID inside of ‘this’

Now we need the binding context path. This is stored in the row of the table that we clicked, which is a few parents up from our current position at the button. In order to locate where it is precisely we can simply console log the source of the event (i.e. the Add Review button) and look through the parent hierarchy until we find the row:

Console log the Add Review Button
Console output

Keep opening the oParent property until you find the row. The tip that you found the row is that it will have an aggregation called “cells” and also it’s sId property should contain LineItem-innerTableRow as well.

Signs that you found the row

Now just count how many levels down you went and call the getParent method that many times. Finally call the method getBindingContextPath on the result and you’ll have it. Note that we don’t cache it since each time we click the button we expect to get a different binding path.

Getting the row’s binding path

Now we just need to create an object to hold both of these values and pass it to our attachBeforeOpen call, as shown below:

Passing in the params

Now we can go to our AddReviewDialogHandler file and confirm that the parameters were received:

Checking params were received
The console output

Success! We have our parameters. Next we’ll build a form and bind this book’s reviews to it so we can create a new review.

Step 3: Creating a Basic Form and Binding the Reviews

First let’s create an empty form in our AddReviewDialog fragment.

The empty form

Here we define a Form with an ID of “addReviewForm”, which we will use to access it from the handler. We set it’s property editable to true as well. Despite what it may appear, this is merely for line formatting purposes and has no impact on whether or not the form is actually editable. Finally, we give the form the title “Review Details”, which will appear at the top of the form. Next, in the aggregation layout we specify ColumnLayout and for the time being accept the default parameters, though later we may tweak them a little bit to get the best visual experience for our users. We can see the result of this below:

The dialog with an empty form

Now we need to bind the selected book’s reviews to the form. For those familiar with SAPUI5’s oData V2 model, the next steps may seem a bit unintuitive, but it’s necessary for working with the V4 model. If you are more comfortable working with the V2 model, feel free to use the oData V2 adapter instead. You can find instructions on how to set it up here.

First, let’s destructure our parameters and then use the dialog ID to allow us to access the form that we just made.

Getting the form

Next we have to use the binding path from our parameters to bind the book’s reviews to the form’s formContainers aggregation, as shown below:

Binding the reviews to the form containers aggregations

We call the bindAggregation method of our form, passing in the name of the aggregation and an object containing the binding information. For now we just specify the path to the data that needs to be bound. For that, we take the binding path for the current book and append ‘/reviews’ as this will point to the reviews of this book. It’s actually the same syntax as if we were typing in the URL to get the data directly from the backend (Books(<uuid>)/reviews).

Currently if we try to open the dialog, though, we get the following error:

Uncaught (in promise) Error: Missing template or factory function for aggregation formContainers of Element sap.ui.layout.form.Form#bookshop::BooksList-AddReviewDialog--addReviewForm !

We got this error because we didn’t tell it what to render with the data we bound. Let’s add a simple form container, element, and input so we can get rid of the error and see what we’re getting.

Creating a form container, form element, and input

Just a brief refresher on SAPUI5 forms to help those who haven’t worked with them before. A Form has many FormContainers. One form container is associated with a specific category of information, for example if we were making a form to input employee information we’d have a container for general info, contact info, address info, etc. Every form container has many FormElements, which contains a label for a specific piece of information and one or more inputs to gather that information. For example, in our employee example, telephone number might have the label “telephone” with 3 separate inputs to capture each part of the phone number separately (XXX-XXXX-XXXX, assuming we’re only dealing with American phone numbers).

In our case, we create a FormContainer and provide it with a single FormElement with the label “Title”. We then put a single Input field inside of the FormElement, binding it to the “title” property of the current review. You may already be able to predict what’s going to happen here, but this is what we get:

Problem: we got all the Reviews!

That’s a problem, isn’t it? Because we bound all of the Reviews to the container, we got an input for every review, and they’re all already full with existing data! That’s not what we want!

You may wonder why we did this in the first place, but in short it’s a quirk of the oData V4 model — it’s designed specifically with handling tables in mind and it doesn’t support this kind of single form entity creation in an explicit way, but it can be done. Let’s learn how.

Step 4: Single Entity Creation With the oData V4 Model

If you search the internet, there aren’t many good solutions out there for how to do single entity creation with the oData V4 model, but I’d like to show my solution here as I think it’s a better than the ones that I’ve found so far (I’ll leave it to you to judge).

The first step is to limit the number of FormContainers created to just one using the length property for our bindAggregation function.

Set the length property to 1

Now, we can see the result:

Limited to 1

Better, but we still have an existing review attached. If we were actually to submit this data, it would update that review. Of course we don’t want that. In order to make sure we get a blank one, we need to get the binding and create a new, blank entry as shown below:

Creating a new entry

First we get the binding using the getBinding method of the form. After that we use the create method of the binding to create a new entry. As an input, we provide an object that matches the shape of our Review model but set all of the properties to blank values. Note, however, that with our current settings this will immediately send a request to the backend to create this entity. We don’t want that — we want our user to input values first. In order prevent automatic submission by the create method, we simply need to set an updateGroupId for our binding, which tells SAPUI5 to not send a create, update, or delete request until that group ID is invoked. Until invoked, SAPUI5 will simple hold on to all the changes but not submit them. Let’s add that:

Adding an updateGroupId

Now, let’s boot up the app again and try clicking the Add Review button:

A dialog with one entry only, and it’s blank

Success! We have one blank entry in our dialog now.

The last thing you may be concerned about here is unnecessary calls to the backend for review data that we will never use. Luckily, this doesn’t seem to be an issue. You can try clicking all the Add Review buttons you want, but check the console output and you’ll find no unnecessary calls for review data.

My console output after clicking all of the buttons once

Conclusion

That’s all for this week. In this episode we learned how to set up a beforeOpen event handler for a dialog, how to pass in custom parameters to that handler, how to bind an entity from our oData V4 model to a form, and how to prevent existing entities from being rendered.

Next week we’re going to finish up the form. I’ll also show you how to utilize the oData V4 metadata object to make sure that our form is responsive to possible changes on the backend and also where to define errors to catch backend changes that can’t be handled by our implementation. Stay tuned!

Was anything unclear in this tutorial? Leave a question below and I’ll get back to you as soon as possible. Was anything incorrect? Please leave a comment below and let me know (a source for the correction would be most helpful). Thanks for your comments!

Support

Did you like this blog? Want to make sure I can keep creating them? Then consider subscribing on Patreon.

--

--

Brian Heise
Brian Heise

Written by Brian Heise

Full Stack web developer employed at Liferay Japan

No responses yet