Implementing Decimal Keypad on iOS in a Hybrid Application (Ionic v1)

Being an early employee at any startup gives you the advantage of working on various things at a time. At Fyle, I was able to work on various front-end applications including web app, extension and mobile app. We use the power of ionic to build android and iOS apps.

For those who are not familiar with ionic

It is an open source UI toolkit for building performant, high-quality mobile and desktop apps using web technologies (HTML, CSS, JavaScript).

Since we are into expense management space and our customers heavily use our mobile app to file expenses on the fly, we want them to have a seamless experience when it comes to entering decimal values for amount. The decimal keyboard works out of the box on android but for long, we were struggling to get the same working on iOS.

Since ionic is built on web technologies, the HTML5 input type="number" markup shows the number keypad. Lets make use of this markup on our ionic app to show the numeric keypad.

<input type="number" ng-model="vm.decimalNumber" />

Android detects the type=”number” format for us and slides up the decimal keypad by default. But the same markup does not work on iOS. Lets take a look at how the keypads looks on each platform when using the above markup.

As you can see from the above screenshot, on iOS this will display a standard keyboard but slightly modified with a row of numbers at the top and some alphabetic keys at the bottom which isn't ideal. So how do we solve this? To overcome this, we can put in a small hack to make iOS display only numbers. Here is how we can do that.

<input type="number" min="0" inputmode="numeric" pattern="[0-9]*" ng-model="vm.onlyNumbers" />

Perfect! This will show up the numeric keyboard on iOS which is what we are expecting. But there is a problem with this approach. The above code works perfectly fine if we want to use a numeric keyboard, but it doesn't show up a decimal keypad. You are restricted to just numbers here and cannot use any decimal numbers.

So what can be done? To overcome this problem we can make use of a cordova plugin that shows a decimal keypad on iOS. We are going to use the plugin called cordova-plugin-decimal-keyboard by mrchandoo.

First let's begin by installing this plugin.

ionic cordova plugin add cordova-plugin-decimal-keyboard

And this is how we use the plugin.

<input type="text" pattern="[0-9]*" decimal="true">

This plugin only works on iOS. If you notice from the above markup we are using type=”text” and for android we use type=”number”. So how do we make this work on both android and iOS? Ionic provides a utility function to check if the platform is android or iOS and we can use this utility to display the required markup selectively.

In the angular controller:

$scope.isAndroid = ionic.Platform.isAndroid();

In the HTML page:

<input type="number" ng-model="decimalNumber" ng-if="isAndroid" /><!-- Android -->
<input type="text" pattern="[0-9]*" decimal="true" ng-model="decimalNumber" ng-if="!isAndroid" /><!-- iOS --> 

So the ionic.Platform.isAndroid(); will return if our app is running on android and we display the input field based on the returned value.

Even though the above code work perfectly fine, there is one catch. The value of decimalNumber on android will be decimal (floating point number), but on iOS it will be a string. This is because we are using type=”text” on iOS and changing this to number will not display the decimal keypad on android. To fix this we have to convert the string to decimal value. We can do this in the save function just before we send out data to the backend.

if (!$scope.isAndroid) {
    // Convert the value from string to float
    $scope.decimalNumber = parseFloat($scope.decimalNumber, 10);
}

And here is how the keypad on our iOS app look like.

Awesome! right? Now that we have got the decimal keypad working on both android and iOS, there is one more problem. Using this code in lots of the places will soon become redundant. And in future if we were to change the plugin we would have to change the code in all the places.

What's the solution?

Let us create a directive to handle all of these irrespective of what platform we are using. Our directive should do the following things for us:

  1. Use the appropriate markup to show up the decimal keypad
  2. Return the value as a floating point number to the ng-model

Lets decide on how our directive code should look and work. We can create attribute directive and use it along with the input field. Our markup looks something like this.

<input type="number" decimal-keypad class="amount" ng-model="decimalAmount" name="amount" ng-min="0.01" placeholder="Amount" required>

Now lets create our directive. Our directive will have 2 files.

  1. decimal_keypad.template.html
  2. decimal_keypad.directive.js

Our directive code looks like this.

<!--- decimal_keypad.template.html -->

<input type="number" />
<!-- decimal_keypad.controller.js -->

;(function () {
  'use strict';
  
  angular
    .module('starter')
    .directive('decimalKeypad', decimalKeypad);

  function decimalKeypad () {
    var directive = {
      restrict: 'A',
      require: 'ngModel',
      replace: true,
      scope: {
      },
      link: decimalKeypadLinkFn,
      bindToController: {
        modelValue: '=ngModel',
        numberValue: '='
      }
    };

    return directive;

    function decimalKeypadLinkFn (scope, element, attrs, ngModel) {
      scope.isAndroid = ionic.Platform.isAndroid();

      if (scope.isAndroid) {
        return;
      }
      
      element.attr('type', 'text');
      element.attr('pattern', '[0-9]*');
      element.attr('decimal', 'true');
      
      scope.onAmountFocus = function () {
        if (ngModel.$viewValue) {
          ngModel.$viewValue = ngModel.$viewValue.toString();
        }

        element.attr('type', 'text');
        element.attr('pattern', '[0-9]*');
      };
      
      scope.onAmountBlur = function () {
        if (ngModel.$viewValue) {
          ngModel.$viewValue = parseFloat(ngModel.$viewValue, 10);
        }
        
        element.attr('type', 'number');
        element.attr('pattern', '');
      };

      element[0].addEventListener('focus', function () {
        scope.onAmountFocus();
      });
      
      element[0].addEventListener('blur', function () {
        scope.onAmountBlur();
      });
    };
  }

})();

Even though the above code is written for Angular JS, the same logic can be applied to any JavaScript code.

So let me explain what the above code does.

  1. Our template will have the default input as type="number" and we are adding events when user focus and blur the input field via the controller.
<input type="number" />

2. Check if the platform is android. If yes, then return without executing the code further.

scope.isAndroid = ionic.Platform.isAndroid();

if (scope.isAndroid) {
    return;
}

3. If the platform is iOS, then we are changing the input type to text, setting the pattern to display numbers and tell the plugin to show the decimal point on the keypad. We are also adding event listeners to the input field to listen for focus and blur events when user clicks on and off the input field.

element.attr('type', 'text');
element.attr('pattern', '[0-9]*');
element.attr('decimal', 'true');

element[0].addEventListener('focus', function () {
    scope.onAmountFocus();
});

element[0].addEventListener('blur', function () {
    scope.onAmountBlur();
});

4. When the user focus on the input field we are changing the model value to string and changing the element's type attribute to text and setting the pattern to display numbers.

scope.onAmountFocus = function () {
    if (ngModel.$viewValue) {
        ngModel.$viewValue = ngModel.$viewValue.toString();
    }

    element.attr('type', 'text');
    element.attr('pattern', '[0-9]*');
};

5. When the user clicks away from the input field we are changing the model value to float and changing the input type to number and removing the pattern attribute. By doing this the value returned to ng-model is always a number and setting the type to number displays it without any error.

scope.onAmountBlur = function () {
    if (ngModel.$viewValue) {
        ngModel.$viewValue = parseFloat(ngModel.$viewValue, 10);
    }

    element.attr('type', 'number');
    element.attr('pattern', '');
};

By following the above logic we can get the decimal keypad to work on ionic v1 app or any hybrid apps.

If you are looking to implement this on your app, please do try out and let us know of any issues.

Sudheer Ranga

Read more posts by this author.