Form validation in UI5
The Problem
A super common request that I've come across in my years building UI5 applications is to have form validation when clicking onto the next page or saving data.
UI5 has plenty of in-line validation to make sure that the data is correctly formatted on entry and that's great but what about when we're interested in is validation after the user clicks that "submit" button.
Below the example we'll use is a simple questionnaire/ form where users submit some personal details but we want to make sure that some of them are required/ entered before allowing a submission.
Example form
In the above screen shot we have our simple form which looks like this in our view/ XML code:
<mvc:View controllerName="nathan.hand.form.validation.controller.mainView" xmlns:l="sap.ui.layout" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
displayBlock="true" xmlns="sap.m">
<App>
<pages>
<Page title="{i18n>title}">
<content>
<VBox width="33%" class="sapUiSmallMargin">
<Label required="true" text="First Name:"/>
<Input id="firstName" value="{data>/firstName}"/>
<Label required="true" text="Surname:"/>
<Input id="secondName" value="{data>/secondName}"/>
<Label text="Email address:"/>
<Input id="emailAddress" value="{data>/emailAddress}"/>
<Label text="What did you think of the event?:"/>
<Input id="eventFeedback" value="{data>/eventFeedback}"/>
<Button press="onSubmit" text="submit"/>
</VBox>
</content>
</Page>
</pages>
</App>
</mvc:View>
Solution 1
This solution is sadly one that I've seen on some inherited projects/ code, It's simple and easy to read but could eventually create a massive amount of unnecessary code.
onSubmit: function(oEvent)
{
var passedValidation = this.validateEventFeedbackForm();
if(passedValidation === false)
{
//show an error message, rest of code will not execute.
return false;
}
//Maybe show a success message, rest of function will execute.
},
validateEventFeedbackForm: function(oEvent)
{
var firstNameId, secondNameId, firstNameValue, secondNameValue;
firstNameId = this.getView().byId('firstName');
secondNameId = this.getView().byId('secondName');
firstNameValue = firstNameId.getValue();
secondNameValue = secondNameId.getValue();
if(firstNameValue === "")
{
firstNameId.setValueState("Error");
return false;
}
else if(secondNameValue === "")
{
secondNameId.setValueState("Error");
return false;
}
else{
firstNameId.setValueState("None");
secondNameId.setValueState("None");
return true;
}
}
This example while it would work is really a terrible way to go about this, imagine if I had 10 fields that were mandatory on this form? The amount of if statements would grow for each further required field and also this simply isn't reusable/ easy to add more fields into the required list without a bunch of work.
solution 2
The next solution makes use of storing a list of ID's inside of the controller and looping through them, not a bad solution but it does require us to maintain a list of ID's in the XML and the controller:
onSubmit: function(oEvent)
{
var requiredInputs = this.returnIdListOfRequiredFields();
var passedValidation = this.validateEventFeedbackForm(requiredInputs);
if(passedValidation === false)
{
//show an error message, rest of code will not execute.
return false;
}
//Maybe show a success message, rest of function will execute.
},
returnIdListOfRequiredFields: function()
{
let requiredInputs;
return requiredInputs = ['firstName', 'secondName', 'emailAddress'];
},
validateEventFeedbackForm: function(requiredInputs) {
var _self = this;
var valid = true;
requiredInputs.forEach(function (input) {
var sInput = _self.getView().byId(input);
if (sInput.getValue() == "" || sInput.getValue() == undefined) {
valid = false;
sInput.setValueState("Error");
}
else {
sInput.setValueState("None");
}
});
return valid;
},
The above snippet is a pretty good, we've managed to keep to our requirements of showing an error value state and can add as many items to the requiredInputs array as we want.
The drawback is that we need to maintain a list of required values in the controller AND we need to set their ID's in the XML.
Solution 3
So what's a better solution than the above? we've consolodated our code and it's not too hard to update an array of id's across pages/ projects right? Well technically yes but I think keeping the logic of what field is required inside of the controller isn't the best and that things such as required fields should be indicated inside of our XML the same way that we set required on our labels.
This is something that I think that SAP is missing, I think we should have an aggregation for all input types as a "required" so that we can simply do something like:
this.getView().getRequireFields();
We don't have that in SAP UI5 so I've come up with a solution using custom data along with some jquery to allow us to identify the input fields that are required and pass them through our loop which requires the inclusion of the core at the top of our XML view as well as adding them to the input fields as shown here:
<mvc:View controllerName="nathan.hand.form.validation.controller.mainView" xmlns:l="sap.ui.layout" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
displayBlock="true" xmlns="sap.m" xmlns:core="sap.ui.core">
<App>
<pages>
<Page title="{i18n>title}">
<content>
<VBox width="33%" class="sapUiSmallMargin">
<Label required="true" text="First Name:"/>
<Input id="firstName" value="{data>/firstName}">
<customData>
<core:CustomData key="required" value="true" writeToDom="true" />
</customData>
</Input>
<Label required="true" text="Surname:"/>
<Input id="secondName" value="{data>/secondName}">
<customData>
<core:CustomData key="required" value="true" writeToDom="true" />
</customData>
</Input>
<Label text="Email address:"/>
<Input id="emailAddress" value="{data>/emailAddress}"/>
<Label text="What did you think of the event?:"/>
<Input id="eventFeedback" value="{data>/eventFeedback}"/>
<Button press="onSubmit" text="submit"/>
</VBox>
</content>
</Page>
</pages>
</App>
</mvc:View>
So what you'll see from the above here is that when we run our program we'll see a new data attribute added to our HTML as shown here:
So how do we get the id's of these fields now that we've indicated that they're required? using a simple bit of jquery as shown below:
returnIdListOfRequiredFields: function()
{
var requiredInputs = [];
$('[data-required="true"]').each(function(){
requiredInputs.push($(this).context.id);
});
return requiredInputs;
},
This will then return to us a list of id's were we've set our custom data attribute of required to true, this means that we can more effectively know in our XML binding/ view what fields are going to be required when we hit submit and we can easily add in more fields in the correct place.
What I like most about this solution is that we don't have to define an ID to our controls inside of our XML as most of the time it's simply not required.
I think that this is the best option when it comes to "on submit" validation as we're defining our required fields inside of the XML rather than maintaining a list of ID's inside of our controller.
conclusion
Solution 1 is a valid solution but not very scalable and will lead to rather messy code if we need to add more fields. There is a use for this approach such as if we only have 1 field on the screen to validate against, but I'd rather we at least use solution 2 as the scope will often change.
Solution 2 is a good solution but we need to maintain a list of id's in our controller which feels slightly less maintainable.
Solution 3 is essentially the same as solution 2 but we get our id's via a more dynamic method and I think is more maintainable/ obvious for my colleagues to takeover from me and keeps the logic in the correct places.
Do you have a better solution that you think we should explore? or a problem with either of my solutions? let me know in the comments down below.
···