[AngularJS] 限制小數位數輸入框 Directive 實作

限制小數位數輸入框 Directive 實作

Fast-Input Directive which provide k,m converting function with changeable decimal place.

前言

在開發金融相關系統時,對於輸入數字的精確度要求會因選擇標的有所差異,因此就需要可隨意切換小數位數上限的功能;此外,金融從業人員習慣鍵入k及m來快速完成1000或1000000倍數轉換,並且數字加註千分為更是必要的顯示方式。

整理一下預計實作之功能如下:

  1. 輸入數字時自動加註千分位 (ex. 1,000)
  2. 在數字末端輸入k或m時,自動轉換數字內容 (ex. 12k => 12,000)
  3. 限制輸入小數位數,且可以動態即時切換。

 

環境

  • AngularJS 1.4.7
  • Chrome 46.0.2490.86

 

設計要點

整個Directive的設計上約略包括下列三大主軸。約略來說就是將model數值(ex. 1000.1234)轉換為view顯示文字(ex. 1,000.1234),接著在view中輸入數字時,即時動態地加註千分位以及處理輸入k或m時所需的數值轉換,並且依據傳入的小數位數上限來做輸入的限制,再將view中呈現的文字(ex. 1,000.1234)轉換為數值(ex. 1000.1234)來存入model中;最後,別忘了要監控外部傳入的小數位數上限,這樣才可動態切換小數位數並反應相對數值。


 

Model to View

ctrl.$formatters.push(myFormatter);

存在於controller的$formatters是一個陣列,當Model值被異動時會被觸發依序呼叫$formatters中所有方法,來將Model數值轉換為特定顯示格式呈現於畫面上;因此我們就可以透過這個機制來自行定義數值呈現的格式,在此我們的需求很簡單,就是透過$filter('number')來將數值加註千分位並限制小數顯示位數。片段示意代碼如下所示。

// [formatter] model -> view
function myFormatter(modelValue) {

  var fractionSize = scope.$eval(attrs.fastInput);
  var strValue = $filter('number')(modelValue, fractionSize);
  
  // remove trailing zeros & dot
  strValue = strValue.replace(/(\.[0-9]*?)0+$/, "$1");  
  strValue = strValue.replace(/\.$/, "");               
  
  return strValue;

}

 

View to Model

ctrl.$parsers.push(myParser);

存在於controller的$parsers是一個陣列,當View值被異動時會被觸發依序呼叫$parsers中所有方法,來將畫面上輸入的文字轉換為特定數值存入Model中;因此我們除了可以透過這個機制來自行定義數值轉換邏輯外,並可以在輸入當下一併控制畫面上需要呈現的文字,限制使用者輸入小數位數之上限。片段示意代碼如下所示。

// [parser] view -> model
function myParser(viewValue) {

  var strViewValue = null;
  var numModelValue = null;


  if (viewValue) {
    
    try {

      // get decimal place
      var fractionSize = scope.$eval(attrs.fastInput);

      // remove thousand separator
      strViewValue = viewValue.toString().replace(/\,/g, '');

      // convert k, m to *1000, *1000000
      strViewValue = getConvertedStrValue(strViewValue);

      // process decimal number
      var numParts = strViewValue.split(".");
      var integerNum = numParts[0] ?  parseInt(numParts[0]).toLocaleString() : '0';
      var decimalNum = numParts[1] ?  numParts[1].substring(0, fractionSize) : '';
      
      // get view value
      strViewValue = 
        integerNum + (numParts.length > 1 ?  '.' : '') + ((decimalNum) ? decimalNum : '');

      // get model value
      numModelValue = 
        parseFloat(strViewValue.toString().replace(/\,/g, ''));

    } catch (e) {

      strViewValue = null;
      numModelValue = null;
      
    }
    
  }


  // change view value
  ctrl.$setViewValue(strViewValue);
  ctrl.$render();

  // return model value
  return numModelValue;
}

 

Detect Changes

scope.$watch(target, doSomethingWhileChanging);

由於需要動態即時地切換小數位數,因此要監控傳入的小數位數(attrs.fastInput)之異動情況,當有異動時重新調整數值。片段示意代碼如下所示。

// detect outside changes and update our input
scope.$watch(attrs.fastInput, function(value) {
  myParser(elem.val());
});

 

實作說明

首先建立 app model 與 test controller。

angular
  .module('app', []);

angular
  .module('app')
  .controller("TestController", function() {
    var vm = this;
    vm.MyNum = 100.1234;
    vm.MyDecimalPlace = 4;
  });

接著實作fastInput directive

angular
  .module('app')
  .directive('fastInput', ['$filter', function($filter) {
    return {
      require: '?ngModel',
      link: function(scope, elem, attrs, ctrl) {

        if (!ctrl) return;

        // [formatter] model -> view
        function myFormatter(modelValue) {

          var fractionSize = scope.$eval(attrs.fastInput);
          var strValue = $filter('number')(modelValue, fractionSize);
          
          // remove trailing zeros & dot
          strValue = strValue.replace(/(\.[0-9]*?)0+$/, "$1");  
          strValue = strValue.replace(/\.$/, "");               
          
          return strValue;

        }

        // [parser] view -> model
        function myParser(viewValue) {

          var strViewValue = null;
          var numModelValue = null;


          if (viewValue) {
            
            try {

              // get decimal place
              var fractionSize = scope.$eval(attrs.fastInput);

              // remove thousand separator
              strViewValue = viewValue.toString().replace(/\,/g, '');

              // convert k, m to *1000, *1000000
              strViewValue = getConvertedStrValue(strViewValue);

              // process decimal number
              var numParts = strViewValue.split(".");
              var integerNum = numParts[0] ?  parseInt(numParts[0]).toLocaleString() : '0';
              var decimalNum = numParts[1] ?  numParts[1].substring(0, fractionSize) : '';
              
              // get view value
              strViewValue = 
                integerNum + (numParts.length > 1 ?  '.' : '') + ((decimalNum) ? decimalNum : '');

              // get model value
              numModelValue = 
                parseFloat(strViewValue.toString().replace(/\,/g, ''));

            } catch (e) {

              strViewValue = null;
              numModelValue = null;
              
            }
            
          }


          // change view value
          ctrl.$setViewValue(strViewValue);
          ctrl.$render();

          // return model value
          return numModelValue;
        }
        
        // convert value which inculde k, m multipliers
        function getConvertedStrValue(strValue) {

          if (isNaN(strValue)) {
          
            var convertedValue = 0;
            var multipliers = {k: 1000, m: 1000000};
            var lastChar = strValue.charAt(strValue.length-1);
            var usedMultiplier = multipliers[lastChar.toLowerCase()];
            if (usedMultiplier) {
              convertedValue = FloatMul(parseFloat(strValue) , usedMultiplier);
            } else {
              convertedValue = 0;
            }
            strValue = convertedValue.toString();
          }

          return strValue;

        }

        // float multiplier
        function FloatMul(arg1, arg2){
          var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
          try { m += s1.split(".")[1].length; } catch (e) { }
          try { m += s2.split(".")[1].length; } catch (e) { }
          return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
        }

        // model -> view 
        ctrl.$formatters.push(myFormatter);

        // view -> model
        ctrl.$parsers.push(myParser);

        // detect outside changes and update our input
        scope.$watch(attrs.fastInput, function(value) {
          myParser(elem.val());
        });
      }
    };
  }]);

測試畫面HTML如下,只需要在input元素中加入fast-input attribute (fastInput Directive)即可。

<html ng-app="app">

  <head>
    <script data-require="angular.js@1.4.7" data-semver="1.4.7" src="https://code.angularjs.org/1.4.7/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body>
    <div ng-controller="TestController as ctrl">
        Please enter number:       <br />
      <input type="text" ng-model="ctrl.MyNum" fast-input="ctrl.MyDecimalPlace" />
      <br />
      <br />

        Number will be: {{ctrl.MyNum}}
      <br />
        Decimal Place: {{ctrl.MyDecimalPlace}}
      <br />
      <br />
      <input type="button" value="digit: 3" ng-click="ctrl.MyDecimalPlace=3">
      <input type="button" value="digit: 1" ng-click="ctrl.MyDecimalPlace=1">
      <input type="button" value="digit: 5" ng-click="ctrl.MyDecimalPlace=5">
    </div>
  </body>

</html>

 

結果呈現

首先測試一下輸入 k 及 m 是否可以正確轉換數值。從以下測試可以發現輸入 k 時,數值會自動乘上 1000,當輸入字尾是 m 時,數值會乘上 1000000,而最終數值是可以正確調整顯示的。

再來測試小數位數切換功能。從以下測試可以發現切換位數時,數值會正確進行調整。

Live Demo Here

 

參考資訊

http://www.chroder.com/2014/02/01/using-ngmodelcontroller-with-custom-directives/

http://kevintsengtw.blogspot.tw/2011/09/javascript.html

 

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !