SAP Tutorial: The Complete CAP Java Part 15
Making Your Frontend Responsive to Backend Changes with oData V4 Metadata
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:
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:
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:
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:
Within the annotations property, we can find what we’re looking for:
We can therefore use the following binding:
Now, if we change the max value in the CDS to 6, for example, we get the following responsive result on the frontend:
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:
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:
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:
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:
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.
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:
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:
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:
And the 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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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 :
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:
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.