Ionic Form Handling & Validation

by - April 02, 2017


Forms are usually one of the major points of interaction between an app and the user, allowing them to send data to the application. Commonly the data is sent to the web server, but the app can also intercept it to use it on its own.

It’s all about user experience!

Forms are also the bottleneck of the conversion funnel, as they require the user to perform a task much more complicated than just a click, by thinking and typing all the required information by the form. This may be much more frustrating in mobile devices and narrow screens, which is why you have to strive to do your best to have an awesome user experience to reduce users frustrations and ultimately improve your funnel conversion.
Before starting, it’s important to take the time and analyze what fields are needed. When designing the mock up, always try to include the fields that are imperative to you, knowing that the bigger the form, the bigger chance you have of losing some users. Keep in mind the simplicity by only asking for what you absolutely need.
It isn’t part of the scope of this article to go beyond the basics of UX of forms, but if you want to learn more about this topic, we recommend these articles:
From a design perspective, Ionic Framework has a wide collection of nice form elements (inputs) for you to choose. On the other hand, from the UX point of view, we are gonna use AngularJS form validation to enhance the experience of our users.
Some say that user experience is often more important than what the app offers itself, being in mobile development for Android or iOS.
A quick enhancement that’s easily implemented to your Ionic App is in the realm of form validation. The Best Practices usually say that you should always validate user inputted data via the back-end and we agree with that statement. However, by validating via the front-end as well, it can make improvements to your user experience.
AngularJS ships with its own validators that work great in Ionic Framework mobile apps. We’re going to check out how to use a few of the AngularJS form validators to make our app significantly better for users.

What to do with the data?

Let’s keep in mind that the final purpose of forms is data collection. There are different ways of sending and handling that data in the server, and depends a lot in your backend tech stack and the nature of your business logic, so we are not going to cover this.
Without further ado, let’s get our hand on with the code!

Hands on!

Dependencies


Step 1: Designing our form UX, understanding our users needs

The objective of this post is to explore UX improvements going through as many form validation examples as possible.
It’s important to take some time to write down your form requirements before starting, in our case this is the data we want to collect with it’s corresponding constraints.
  • Username
    • Min length (5)
    • Max length (25)
    • Must contain numbers and letters
    • The username must be unique
    • Required
  • Name
    • Required
  • Surname
    • Required
  • Email
    • Valid email
    • Required
  • Country selection
  • Phone
    • The phone must be valid for the selected country
    • Should accept only numbers
    • Required
  • Gender
  • Password
    • Min length (5)
    • Must contain letters (both uppercase and lowercase) and numbers
    • Must re-type password to ensure correctness
    • Required
  • Terms Checkbox
    • Must accept terms
Angular form validation is a great starting point, but we are gonna push the limits and explore other custom validations and interactions to increase the user experience of our app.

Validation timing - When should we do the validation?

The key is to not interrupt and annoy the user. That’s why it’s not recommended to validate when the user submits the form. Imagine that panorama, the user spent time filling in each input (without knowing the constraints beforehand) and finally when he thinks the task is done, you show him multiple error messages caused by invalid inputs.
Frustrating! It’s much better to consider the other three options.
Real time: It means validations as you type. These are super handy and are the default for angular validations.
On blur: It may be less annoying for certain cases as we only validate when the user focus out from the input. On the other hand, the user will notice the errors once he moves to the next input, causing him a terrible user experience.
Debounce: We can also set a debounce to run validations after some time (ex: 500 milliseconds). This is a great alternative to real time and on blur.
You can find more information about validation timings here.

Basic validations

As we mentioned before, there are several validations that came out of the box with angular. Some of the most important ones are the following:

required

This validates that the input is not empty, and also that the value matches the type of the input.
This means for example that if the input is of type “email”, then the input will be valid if it’s not empty and the value is an email (aaa@aaa.com). Some smartphones recognize the email type, and adds ".com" to the keyboard to match email input.
Learn more about input types here.

ng-required

This one differs a little bit from the previous one, by allowing you to set an expression to validate against.
In this example the input will be valid if the user accepts the terms:
<input type="checkbox" name="terms" ng-model=”user.accept_terms” ng-required="!user.accept_terms"> I accept terms

ng-minlength

Validates that the value is longer than minlength.

ng-maxlength

Validates that the value is shorter than maxlength.

ng-pattern

Validates that the input value matches the specified RegExp.
Find more information about RegExp patterns here and here.
Feeling curious?, find more information about angular validation directives here.

Advanced validations

If we want to go the extra mile, we can get very creative and build any kind of directives for custom validation.
In this post we will be implementing three in particular. One to validate that the username is unique (checks against the server if the username is already taken), another that implements the confirm password validation to ensure the user typed the desired password correctly, and a third one that validates that the phone is valid for the selected country.
You can find more information about angular custom validations here.

Other user experience considerations

It’s also important to display error messages to guide the user in the correct direction. Lets explore two alternatives.

ng-show

The easiest one is to show/hide appropriate error messages for each validation case. Simple yet correct.

ngMessages

In the recent angular release they introduced this new tool. ngMessages is a directive that is designed to show and hide messages based on the state of a key/value object that it listens on.
The directive itself complements error message reporting with the ngModel $error object (which stores a key/value state of validation errors).
In my personal experience for mid-complex use cases, you'll end up using ng-show alongside with ngMessages (For example if you don’t want the user to see the error message before the error occurs and prefer to hide it until the input is not pristine).
I also don’t find any justification to add this extra dependency to your app (it doesn’t come with angular out of the box like ng-show).
Feel free to use it and if you want more information, check this link.
It’s also important to understand the different possible states of our form. The available form properties are as follows:
PropertyDescription
$validA boolean based on whether your rules are valid or not
$invalidA boolean based on whether your rules are invalid or not
$pristineTrue if the form or input value has not been used yet
$dirtyTrue if the form or input has been used
$touchedTrue if the input has been blurred

Step 2: Show me the code

After all the introduction to the tools we have to perform form validations, so now let’s get our hand to the code.

Username

<!-- Username -->
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : sample_form.username.$invalid && !sample_form.username.$pristine }">
    <input type="text" placeholder="Username" name="username" ng-model="user.username" ng-model-options="{ debounce: 500 }" class="form_control" ng-minlength="5" ng-maxlength="25" ng-pattern="/^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$/" valid-username="user.username" required>
  </label>
  <p ng-show="sample_form.username.$invalid && !sample_form.username.$pristine && !sample_form.username.$error.minlength && !sample_form.username.$error.maxlength && !sample_form.username.$error.pattern && !sample_form.username.$error.validusername" class="help-block">Your username is required.</p>
  <p ng-show="sample_form.username.$error.minlength" class="help-block">Your username is too short.</p>
  <p ng-show="sample_form.username.$error.maxlength && !sample_form.username.$error.pattern" class="help-block">Your username is too long.</p>
  <p ng-show="sample_form.username.$error.pattern && !sample_form.username.$error.minlength" class="help-block">Your username must contain numbers and letters.</p>
  <p ng-show="sample_form.username.$error.validusername" class="help-block">Your username has already been taken.</p>
</div>
We used ng-minlength="5" and ng-maxlength="25" to ensure the minimum and maximum length of the value. We also used required to avoid this input to be left empty, and ng-pattern="/^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$/" to force a correct input value containing at least one uppercase, one lowercase and one number.
Notice how we used the valid-username="user.username" custom directive to validate that the username is not already taken.
This is the implementation of the custom directive, as we don’t have a dedicated backend and database for this example we hardcoded two existing usernames (“abc123” and “123abc”).
.directive('validUsername',function(){
  return{
    require: 'ngModel',
    link: function(scope, ele, attrs, c){
      scope.$watch(attrs.ngModel, function(username_value){
        if(username_value=="abc123" || username_value=="123abc")
        {
         c.$setValidity('validusername', false);
        }
        else
        {
         c.$setValidity('validusername', true);
        }
      });
    }
  };
})
Last but not least, notice how we used different error messages corresponding to each validation.

Name

<!-- Name -->
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : sample_form.name.$invalid && !sample_form.name.$pristine }">
    <input type="text" placeholder="Name" name="name" ng-model="user.name" class="form_control" required>
  </label>
  <div ng-show="!sample_form.name.$pristine" ng-messages="sample_form.name.$error">
    <p ng-message="required" class="help-block">Your name is required.</p>
  </div>
</div>
Just used required to avoid this input to be left empty.

Surname

<!-- Surname -->
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : sample_form.surname.$invalid && !sample_form.surname.$pristine }">
    <input type="text" placeholder="Surname" name="surname" ng-model="user.surname" class="form_control" required>
  </label>
  <p ng-show="sample_form.surname.$invalid && !sample_form.surname.$pristine" class="help-block">Your surname is required.</p>
</div>
Just used required to avoid this input to be left empty.

Email

<!-- Email -->
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : sample_form.email.$invalid && !sample_form.email.$pristine }">
    <input type="email" placeholder="Email" name="email" ng-model="user.email" class="form_control" required>
  </label>
  <p ng-show="!sample_form.email.$pristine && sample_form.email.$invalid" class="help-block">Enter a valid email.</p>
</div>
Just used required to avoid this input to be left empty. Notice in this case as the input type is email, angular will automatically validate the value is an email (aaa@aaa.com).

Phone

<!-- Phone (+ country) -->
<div class="form_group">
  <label class="item item-input item-select">
    <div class="input-label">
      <span class="placeholder-values">Country</span>
    </div>
    <select ng-options="item as item.name for item in countries track by item.code" ng-model="user.phone.country"></select>
  </label>
</div>
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : (sample_form.phone.$invalid && !sample_form.phone.$pristine)}">
    <span class="placeholder-values">{{user.phone.country.code}}</span>
    <input type="tel" ng-model="user.phone.number" name="phone" placeholder="Phone" class="form_control" ng-pattern="/^[0-9]+$/" valid-phone country="user.phone.country" required>
  </label>
  <p ng-show="sample_form.phone.$invalid && !sample_form.phone.$pristine && !sample_form.phone.$error.validPhone && !sample_form.phone.$error.pattern" class="help-block">Your phone is required.</p>
  <p ng-show="sample_form.phone.$error.validPhone && !sample_form.phone.$error.pattern" class="help-block">Enter a valid phone.</p>
  <p ng-show="sample_form.phone.$error.pattern" class="help-block">Only numbers are allowed.</p>
</div>
This is one of our custom validation directive (valid-phone country="user.phone.country"), we validate the phone against the selected country using Google libPhoneNumber library.
.directive('validPhone',function(){
  return{
    require: 'ngModel',
    scope: {
   country: '='
  },
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$validators.validPhone = function(modelValue, viewValue) {
        if (ctrl.$isEmpty(modelValue)) {
          // consider empty models to be valid
          return true;
        }

        try{
          var phoneNumber = "" + modelValue + "",
              region = scope.country.iso,
              phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance(),
              number = phoneUtil.parse(phoneNumber, region),
              isValidNumber = phoneUtil.isValidNumber(number);
        }catch(e){
          console.log(e);
          return false;
        }

        return isValidNumber;
      };

      // If the user changes the country, then we have to re-validate the phone number
      scope.$watch('country', function() {
        if(scope.country) ctrl.$validate();
      });
    }
  };
})
The approach used for this validation directive differs from the valid-username one. Just wanted to show you a different way to solve the same problem.
We also used required to avoid this input to be left empty, and ng-pattern="/^[0-9]+$/" to only allow numbers for this input (just to prevent weird errors when validating “098185518…..” phone numbers).

Password

<!-- Password (+ confirm password) -->
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : sample_form.password.$invalid && !sample_form.password.$pristine }">
    <input type="password" placeholder="Password" name="password" ng-model="user.password" ng-minlength="5" ng-pattern="/(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z])/" required>
  </label>
  <p ng-show="!sample_form.password.$pristine && sample_form.password.$invalid && !sample_form.password.$error.minlength && !sample_form.password.$error.pattern" class="help-block">Your password is required.</p>
  <p ng-show="sample_form.password.$error.minlength" class="help-block">Your password is too short.</p>
  <p ng-show="sample_form.password.$error.pattern && !sample_form.password.$error.minlength" class="help-block">Your password must contain one lower and uppercase letter, and one non-alpha character.</p>
</div>
<div class="form_group">
  <label class="item item-input" ng-class="{ 'has_error' : (sample_form.confirm_password.$invalid && !sample_form.confirm_password.$pristine) || match_password }">
    <input type="password" placeholder="Confirm Password" name="confirm_password" ng-model-options="{ debounce: 500 }" ng-model="user.confirm_password" confirm-password="user.password" required>
  </label>
  <p ng-show="sample_form.confirm_password.$invalid && !sample_form.confirm_password.$pristine && !sample_form.confirm_password.$error.confirmPassword" class="help-block">You must confirm your password.</p>
  <p ng-show="sample_form.confirm_password.$error.confirmPassword" class="help-block">Your password does not match.</p>
</div>
We used ng-minlength="5" to validate the minimum length of the value. We also used required to avoid this input to be left empty, and ng-pattern="/(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z])/" to force a correct input value containing at least one uppercase, one lowercase and one non alphanumeric character.
We also used our third custom directive (confirm-password="user.password") to ensure both passwords match.
.directive('confirmPassword',function(){
  return{
    require: 'ngModel',
    scope: {
   password: '=confirmPassword'
  },
    link: function(scope, elm, attrs, ctrl){
      ctrl.$validators.confirmPassword = function(modelValue, viewValue) {
        if (ctrl.$isEmpty(modelValue)) {
          // consider empty models to be valid
          return true;
        }

        return (modelValue === scope.password);
      };

      // If the user changes the password, then we have to re-validate the confirm password input
      scope.$watch('password', function() {
        if(scope.password) ctrl.$validate();
      });
    }
  };
})

Terms & Conditions

<!-- Terms & Conditions -->
<div class="form_group">
  <ion-checkbox ng-model="user.accept_terms" name="terms" class="terms-checkbox" ng-required="!user.accept_terms">
    <span class="terms-checkbox-label">I have read and agree to the Terms and Conditions</span>
  </ion-checkbox>
  <p ng-show="!sample_form.accept_terms.$pristine && sample_form.accept_terms.$invalid" class="help-block">Your must accept Terms and Conditions.</p>
</div>
We used ng-required="!user.accept_terms" to ensure the user accepts the terms and conditions.

Finally, submit the form!

<!-- Finally, submit! -->
<button class="button button-full button-positive" ng-click="submit(user)" ng-disabled="sample_form.$invalid">
  Submit
</button>
And finally, just enable the submit button if the whole form is valid using ng-disabled="sample_form.$invalid".

You May Also Like

0 comments