Create responsive number directive with angular and bootstrap

I am working on a directive that will allow a numeric field to be shown in a friendly way based on the amount of space available for that number, so if you have a field with a value of 10,000,000,000 it will then show it as 10B (B for billion) if not enough spaces for the total value of 10,000,000,000.

I wanted this to work with bootstrap. The best approach I could think of was to bind the element in the parent to be sized when loaded (and then the target will react accordingly). I was able to get this to work, but I think there is a better approach.

Here's an example HTML (the directive is the "responsive number" attribute):

<div class="container-fluid">
    <div class="row">
        <div class="col-md-4">Some Label: </div>
        <div class="col-md-8">
            <div responsive-number ng-model="someValue"></div>
        </div>
    </div>
</div>

      

The directive on initial rendering sets the maximum width of the element to the parent node's innerWidth (). The limitation here is that you cannot have any other element contained in the parent node, so I believe there should be a more efficient, perhaps more passive way to approach this by responding to the mutable element of the react number (I ' m is just not sure how to do this, forcing the react number element to stay within the parent container).

Below is the javascript that works for me based on the above HTML. I found this value to be too large for a responsive number container by setting the text overflow style in such a way that the overflow is hidden by clipping - this is where the parent container resides as I need to set the maximum element size to the size of the parent container:

A jsfiddle example can be found here, but modified slightly to use the jQuery event for convenience (but note that I know this is bad practice): http://jsfiddle.net/F52y5/63/

'use strict';

function MyCtrl($scope) {
    $scope.someValue = 100000000000000000;
}

app.directive('responsiveNumber', ['$filter', function ($filter) {
    return {
        require: 'ngModel',
        restrict: 'A',
        scope: true,
        link: function (scope, element, attrs, ctrl) {
            var scrollWidth = element[0].scrollWidth;
            var parentWidth = getParentWidth();
            var displayFriendlyValue = false;

            initializeResponsiveNumber();
            ctrl.$render();

            ctrl.$render = function (isResizeEvent) {
              var temp = shouldRenderFriendlyValue();

              if(isResizeEvent){
                // then the window was resized and the element is already formatted
                // so if it fits, then that all she wrote..
                if (displayFriendlyValue == temp) {
                    return;
                }
              }

              displayFriendlyValue = temp;
              var viewValue = getTransformedViewValue(ctrl.$modelValue, displayFriendlyValue);
              ctrl.$viewValue = viewValue;
              element.html(ctrl.$viewValue);

              // after we've formatted the number it may not fit anymore
              // so if shouldRenderFriendlyValue() previously returned false
              // that means the unformatted number was not overflowing, but
              // the formatted number may overflow
              if (!displayFriendlyValue) {
                  // we check shouldRenderFriendlyValue() again
                  // because sizing can change after we set 
                  // element.html(ctrl.$viewValue);
                  displayFriendlyValue = shouldRenderFriendlyValue();
                  if (displayFriendlyValue) {
                    viewValue = getTransformedViewValue(ctrl.$modelValue, displayFriendlyValue);
                    ctrl.$viewValue = viewValue;
                    element.html(ctrl.$viewValue);
                    return;
                  }
              }
            };

            function getTransformedViewValue(modelValue, displayFriendlyValue){
              var result;
              // could add support for specifying native filter types 
              // (currency, for instance), but not necessary for this example
              if(displayFriendlyValue){
                result = makeFriendlyNumber(modelValue, 'number');
              } else {
                result = $filter('number')(modelValue);
              }

              return result;
            }

            function cleanNumber(num) {
                return (Math.round(num * 10) / 10);
            }

            function makeFriendlyNumber(num, filter) {
                var result;

                if (num >= 1000000000) {
                    result = $filter(filter)(cleanNumber(num / 1000000000)) + 'B';
                } else if (num >= 1000000) {
                    result = $filter(filter)(cleanNumber(num / 1000000)) + 'M';
                } else if (num >= 1000) {
                    result = $filter(filter)(cleanNumber(num / 1000)) + 'K';
                } else {
                    result = $filter(filter)(num);
                }

                return result.toString().replace(/(\.[\d]*)/, '');
            }

            function initializeResponsiveNumber() {
                element[0].style['overflow'] = 'hidden';
                element[0].style['-moz-text-overflow'] = 'clip';
                element[0].style['text-overflow'] = 'clip';

                updateElementSize(parentWidth);

                var debouncedResizeEvent = $scope.$on('debouncedresize', function (event) {
                    scope.$apply(function () {

                        var newParentWidth = getParentWidth();
                        if (newParentWidth == parentWidth) { return; }

                        parentWidth = newParentWidth;
                        updateElementSize(parentWidth);
                        ctrl.$render(true);
                    });
                });

                $scope.$on('$destroy', debouncedResizeEvent()};
            }

            function getParentWidth() {
                var innerWidth = angular.element(element[0].parentNode).innerWidth();
                return innerWidth;
            }

            function shouldRenderFriendlyValue() {
                scrollWidth = element[0].scrollWidth;
                if (element.innerWidth() < scrollWidth) {
                    return true;
                }

                return false;
            }

            function updateElementSize(width) {
                element[0].style.width = width + 'px';
            }
        }
    }
}]);

      

Any suggestions would be much appreciated!

It's also worth noting that I know that in some cases this function takes longer than necessary, but there is no point in optimizing it if there is a better approach.

--------------- update -------------

There was some confusion here, so I wanted to try to simplify the problem statement:

how do i know if there is enough room to display the full value or if i should truncate it?

I've already demonstrated one way to approach this, but I'm not sure if this is the best way.

+3


source to share


2 answers


My recommendation: Don't do this. Some of the reasons are:



  • Responsive design should be monitored and consistent, not chaotic

    If you approach responsive design as you suggest here (adjust elements individually based on runtime allocated space), you end up with little understanding of the different results (because there will be too much opportunity). Also, the results can be quite inconsistent (for example, different digits have different widths - numbers of the same number of digits with the same amount of space may or may not be truncated).

    It's better to just give your content enough space. If you know you are out of space, you can provide multiple content alternatives and switch to an alternative based on screen size (sequentially). For example. on 480px wide screens, you can hide the "long numbers" alternative and display the "short number" instead.

  • Content formatting should aim for optimal readability

    Large numbers are usually not very readable. You can shorten them (and provide units) for better readability in general, and not just in places where they cannot fit into their extended format.

    Keep in mind that users don't just read one number. Comparing multiple numbers, for example, is a common use case, and it can hurt a lot with inconsistent formatting.

  • Observing measurements based on each item can hurt performance

    AFAIK, there is no event to notify you of element dimension changes, and many things can trigger such changes (for example, adding an unbound element to the DOM can trigger different styles). Therefore, monitoring changes requires some periodic checking, which can have an impact on performance when used on a large number of items.

+1


source


It seems to me that you can just do it with a filter. This is just a rough idea of ​​how this might work, and I have not considered the 1000-9999 range for brevity of code, but it shouldn't be much of a problem to work.



/**
 * var value = 12345678;
 *
 * value|niceNumber     => 11M
 * value|niceNumber:2   => 17.73M
 */
 app.filter('niceNumber', function () {
    return function (num, precision) {
        if (isNaN(parseFloat(num)) || !isFinite(num)) { return '-'; }
        if (typeof precision === 'undefined') { precision = 0; }
        var units = ['bytes', '', 'M', 'B', 'T', 'P'],
            number = Math.floor(Math.log(num) / Math.log(1000));

        return (num / Math.pow(1000, Math.floor(number))).toFixed(precision) + units[number];
    };
});

      

0


source







All Articles