SAP Tutorial: The Complete CAP Java Part 15

Brian Heise
13 min readOct 7, 2021

--

Making Your Frontend Responsive to Backend Changes with oData V4 Metadata

Photo by Pankaj Patel on Unsplash

If you’ve been following for a while, you know the drill already but for newcomers, this tutorial series is teaching how to build applications using SAP’s CAP for Java framework. Throughout the course of this tutorial we’re rebuilding SAP’s sample bookshop application, while adding some changes/improvements along the way.

In the last few chapters we’ve been improving a feature from the sample app: a button on a table of books that allows users to add a review of the book. We’ve built the button, the dialog that it opens, and the form in the dialog and we can even submit our data with no problem. However, we’ve hard coded a lot of the configuration for our form, so it won’t respond to changes in the CDS on the backend. This is of course a problem because CDS is supposed to be the single source of truth when it comes to information about our domain model. Let’s how to link the two up.

Step 1: Make the Rating Indicator Responsive to CDS Changes

First, let’s make the rating indicator responsive to the CDS. Recall way back that we created an enum type in CDS to specify the values for the rating field of our Reviews entity, as shown below:

Our rating type

This seemed fine when we set it up, but actually modeling a rating is not a good use case for an enum type like this, especially when working with the SAPUI5’s RatingIndicator component. This is because a rating requires a range from 0 to any other larger number, but enum types can skip values — we could define an enum as 0, 1, 3, 4, 5. If we try to use this to define our RatingIndicator, this could result in the possibility of submitting an invalid value of 2. We could of course make this work with some checks on the frontend but it would be a lot of trouble. Simply converting this to a range would be much easier, so let’s do that:

Converting Rating to a range

Note that we’ll have to remove the assert.range annotation from reviews.cds as well because it will overide the one in our type definition:

Remove assert.range from the Reviews entity

Now we just need to bind the maxValue property of the RatingIndicator to the maximum value of our range using the following syntax:

"{<fieldName>##<annotationName>}"

Our field name is rating of course but to get the annotation name we need to check the metadata. To do that we can go to one of our event handlers (it doesn’t matter which as long as you have access to the event object) and try console logging this:

oEvent.getSource().getModel().getMetaModel().getData();

You’ll find the following result:

Notice the $Annotations property

Within the annotations property, we can find what we’re looking for:

@Org.OData.Validation.V1.Maximum

We can therefore use the following binding:

Binding

Now, if we change the max value in the CDS to 6, for example, we get the following responsive result on the frontend:

6 stars

Let’s take a moment to step aside and create a helper object to contain our annotation strings. After all, these are quite verbose and can clutter up our code a little. In our custom folder let’s create a file called annotations.js and put the following inside:

Annotations object

From now on we’ll add all the annotations that we need to use here so we can easily access them. We can then import this object and use it in our handler, like shown below:

Refactoring to our annotations object

Much cleaner, right?

The next thing we want to do is ensure that the minimum value is 0 since that’s what the RatingIndicator expects. If the minumum value were set to 1 for example, then we could end up submitting invalid data because the rating indicator doesn’t allow setting the minimum value — it’s fixed at zero. The indicator would therefore allow submitting 0 even though would be invalid on the backend.

We’d like our frontend to be able to warn another developer if they happened to not know this fact and tried to change the rating type that we defined in CDS to have a minimum value of something other than zero. Let’s look up the minimum annotation (it’s “@Org.OData.Validation.V1.Minimum”), add it to our annotations object, then implement a composite binding and a formatter function:

Composite binding and formatter

By using the parts property for the binding info, we are able to pass in an array of values, in this case our maximum and minimum values. Next we use the formatter function to check the value of minimum and throw an error if it’s not 0. Otherwise, we return the max value:

Our min value validation function

Note that we check for the value of max before throwing the error because it’s possible that this formatter could be triggered with no data passed in yet (meaning both are undefined). We don’t want to throw any errors in this case. Finally, we return max or 0 because if max happens to be undefined we could end up triggering an error because the RatingIndicator expects an integer value for the maxValue property.

There we have it! Our RatingIndicator is now responsive to the backend, and it will also error out if the backend changes in a way that is incompatible with it.

Next, let’s consider the fact that the comment and title fields are required on the backend but our frontend UI doesn’t reflect this.

Step 2: Setting Fields as Required Using the Metadata Object

If we go back to our Review entity, we’ll notice that we have mandatory annotations on title and text.

mandatory annotations

These provide backend validations to ensure that this data is provided before saving it to the database and rejecting the save if it isn’t present. However, this is not yet reflected in the UI — we don’t have an asterisk next to required fields and we also don’t disable the submit button when a required field isn’t provided. Let’s make sure we do that.

First, we need to locate the property in the metadata object. It can be found as shown below:

The mandatory annotation

Let’s make sure we add this annotation to our annotations object. Also, for the data binding we’ll need the FieldControl annotation as well, so let’s add that too. Our annotations object now looks like this:

The annotations object

Next, we need to bind the Manadatory property to our title and comment elements and also provide a formatter to convert the string to a boolean since the required property expects that. Here’s an example with the title element:

Binding the mandatory property

And the formatter function:

isMandatory formatter function

Now, if the Mandatory annotation is present, we’ll get an asterisk next to the input label indicating that. Also, if we decide, for example, to make the comment field optional in the future, the frontend will respond to the change appropriately and not display the asterisk. Here’s what the UI looks like after implementing this logic in both the title and comment fields. Note that for this example I removed the mandatory annotation for the comment from the CDS to show that this logic works:

Mandatory field implemented

Now we have one last issue: we can still submit the form even when the required fields are blank. Let’s handle that in the next step.

Step 3: Validating Required Fields

The trick to validating the required fields is that we need to use the validation handling logic that we already wrote in the onValidationSuccess and onValidationFail handlers. To do that, we can set up some change events to validate the inputs and if there’s an error, we can trigger those two functions using fireValidationSucces and fireValidationError methods.

First, let’s write the handler function for the change events. Since this validator function could potentially be used in many files, let’s make a seprate validators object in a new file, validators.js:

Throws a validation error if required is true and field is blank

In this function we start by retrieving the input field from the event object. After that, we retrive the value of the required field using the getRequired function. If the input is required, we run the validation logic; if not we do nothing. We include this step on the chance that the decision was made to make one of these fields optional we wouldn’t have to update any code on the frontend — it would continue to work just the way it is.

If the input is required, we extract the value property and check if it’s falsey. If it is, then we call fireValidationError on the input and pass an object with the relevant info, namely the input element itself, the property that failed validation, and a message to be displayed in the valueStateMessage property (the message that displays on the field itself when we throw an error. Note that we also extract the label and use a template string so our error message can state directly what field can’t be blank.

Finally, if the value isn’t falsey, we call fireValidationSuccess and pass in a similar object to the one above but without passing a message because no message is needed if there’s no error.

That’s all we need for the basic logic. The validation logic we wrote earlier will handle the rest. Now we just need to attach this handler to the title and comment inputs. Note that we will need to attach it using two different events: change and liveChange. We need liveChange because we want to apply or remove the error as soon as the user changes the input to blank or adds a single character to remove it; with regular change the user would have to click on another spot in the screen before the validation would update.

Nonetheless, we still need change. This is because SAPUI5 runs it’s own validation logic on change and if it doesn’t detect an error, it will remove our error even if the field is still blank. Therefore, the title and comment inputs need to attach our validateBlank function to both of these events, as shown below:

Attaching the validations

With this we’re almost done, but there’s still a problem: when we open the dialog for the first time, the submit button isn’t disabled, meaning that the user can still submit an empty title and comment. That’s no good!

Fixing this issue is a little tricky, though. The reason is intuitively we would think that we need to check this in the beforeOpenDialog function, but actually in that function the inputs won’t yet have been properly bound with their required property so we won’t actually be able to fire the validation error from there.

The actual event that we care about is when the binding for the required property changes. However, when we first initialize the object it is undefined so we can’t simply attach it then. It only becomes defined after the model context for the input changes so we have to do it then. Therefore, we need to call attachModelContextChange on the input and pass in a function that will check if a binding exists for the required attribute; if the binding exists, we should then call attachChange on it and pass in our notBlank validator function. This is what the function looks like:

Our handler for attaching the initial validation

Note that we detach the change event before attaching it in case we end up inside this if block again at a future time. We remove it and then attach it again so we don’t accidentally attach it multiple times.

It looks almost done but we still have some problems. Firstly, this change event’s source is the requiredBinding, not the input. This means that we won’t actually be able to fire the validation like this. We need a parameters object that contains the input, as shown below:

With a parameters object

Next, we need to adjust our function to handle situations where the input comes from the event object or the parameters object. Here’s the update:

Handling the parameters object

With this change, we add the oParams input and destructure it to get out the input. However, we set the default value of oInput as oEvent.getSource(); that means that if no input is provided through oParams, we’ll try to get it from the event object. Also, we set oParams to an empty object so that an undefined oParams won’t throw an error.

Now if we run it we almost get what we want, but you’ll notice a problem:

Error messages on dialog load!

We get the error messages before the user has had a chance to enter anything. It’s not horrible, but it isn’t exactly a good user experience. We need to update our validation handler to provide a different result on the initial load. For that, we add a property to our parameters object called isInitial and set it to true.

Add isInitialto paramters object

Next, in the validation handler, we need to make sure we don’t provide a message or a property when we fire the validation error; this will prevent an error from being displayed while still allowing our button to be disabled:

Setting logic for initial validation

First we destructure out the isInitial property and default it to false if it doesn’t exist. Next we set the property and message properties to null if we’re in the initial state. This results in the following on the initial load:

Button is disabled but no error messages are displayed

Success! We almost have a fully functioning form. There’s just a small problem with the current implementation: our notBlank handler fires a validation success whenever the field isn’t blank, not just when we switch from blank to not blank. This means that it fires a success even when we switch from being the right length to being too long, which overwrites the default input length validation that SAPUI5 gives us out of the box. We therefore need to update the implementation to check whether the previous value was falsey as well before fireing the validation success. Luckily SAPUI5 input components store this info by default so we just need the following code to clear things up:

Protecting the standard validation

On line 11 we extract the last value using the getLastValue method. On line 23 we add an extra condition before firing validation success: we don’t just check for the value’s existence but also check to make sure that the previous value was falsey. You’ll also notice than on line 21 we manually set the last value of the input to an empty string. This is to overcome a quirk of SAPUI5. It normally will automatically update the last whenever the value changes, but for some reason it doesn’t do this when the current value is falsey. This will lead to a bug when we have a value that’s too long and then completely delete it — the validation won’t clear properly when we finally do add in a new character. This will handle that edge case.

Step 4: Making Labels Responsive to CDS Changes

Up to now we’ve hard-coded the labels for these inputs, but you may probably recall from an earlier post in this series that we can provide labels for fields using CDS. As you might have guessed, we can also use such labels here and in fact we should. After all, we will definitely use the Reviews entity in other parts of our application and we want to make sure our labels remain consistent between our custom components and FioriElements. So let’s define some labels in CDS and then bind them to our form.

Open app/common.cds and add the following annotations to create labels :

Adding labels

This will attach label annotations to these fields. Now we can simply bind them using the label annotation: “@com.sap.vocabularies.Common.v1.Label”. First we’ll add that to our annotations object, then bind the labels as shown below:

Binding the label annotation

Great! Now our custom component will use the same label that will appear later in other parts of our application as well.

Conclusion

We’ve finally finished our custom component for adding reviews on the browse books list report page. As I said at the beginning, this kind of component takes quite a lot of time to implement compared to a standard Fiori Elements function and therefore it should be avoided if possible. Nonetheless, Fiori Elements cannot handle all situations, so I hope this will give you a taste of what it takes to handle such situations.

So what’s coming up next week? Well, up to now we’ve been coding all of our fields in English, but naturally our SAP apps are usually intended for large companies that operate in many countries using many different languages. Therefore, we can’t just leave our app hard-coded in English. In next week’s installment we’re doing to take a deep dive into internationalization in CAP, specifically how to internationalize static values like labels and other UI messages and fields, and also how to internationalize database data. 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

Responses (1)