Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

creating a "responsive number" directive with angular and bootstrap

I'm working on a directive that will allow a numeric field to be displayed in a friendly way based on the amount of space available for that number so if, for instance, you have a field with a value of 10,000,000,000 this would then show as 10B (B for billion) if there is not enough space for the full value of 10,000,000,000.

I wanted this to work with bootstrap. The best approach I could think of involved wrapping the element in a parent element which would be sized by bootstrap (and then the target element would react accordingly). I was able to get this working, but I imagine there's a better approach.

Here is an HTML example (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, upon initial rendering, sets the element's max-width to the parent node's innerWidth(). A limitation here is that you can't have any other element contained within the parent node so I figure there must be a better, perhaps more passive, way to approach this by responding to the responsive-number element being resized (I'm just not sure how to do this while forcing the responsive-number element to remain within the bounds of the parent container).

Below is the javascript that is working for me based on the above HTML. I detect that the value is too large for the responsive-number container by setting the a text-overflow style such that the overflow is hidden via clipping - this is where the parent container comes in as I need to set the max-size of the element to the size of the parent container:

A jsfiddle example can be found here, but is slightly modified to use a jQuery event for the sake of convenience (but note I am aware this is a 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's 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!

Also worth noting I'm aware this function renders more times than necessary in some cases - but there's no sense in optimizing it if there's a better approach.

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

There has been some confusion here so I wanted to try to simplify the problem statement:

how do I know if there is enough space to show the full value or if I have to abbreviate it?

I've already demonstrated one way to approach this, but I'm not convinced it's the best way.

like image 506
Jordan Avatar asked Aug 27 '26 14:08

Jordan


1 Answers

My recommendation would be: Don't do this. Here are some of the reasons:

  • Responsive design should be controlled and consistent, not chaotic

    If you approach responsive design the way you're suggesting here (adjust elements individually based on their run-time allocated space), you'll end up with little to no idea about the various results (because there will be too many possibilities). Additionally, the results may be pretty inconsistent (e.g. different digits have different widths - numbers of same number of digits with the same ammount of space may or may not be shortened).

    It's better to just give your content enough space. If you know you're running out of space, you might provide multiple alternatives of content and switch to an alternative based on display size (consistently). E.g. on screens under 480px wide, you might hide the "long number" alternative and show the "short number" instead.

  • Content formatting should aim for optimal readability

    Large numbers are generally not very readable. You might want to shorten them (and provide units) for better readability in general, not only in places where they can't fit in their expanded format.

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

  • Watching dimensions on per-element basis can hurt performance

    AFAIK, there's no event notifying you of element dimension changes and a lot of things can trigger such changes (for example adding a non-related element to DOM might trigger different styling). Watching changes therefore requires some sort of periodic checking, which can have performance impact when used on a large number of elements.

like image 157
hon2a Avatar answered Aug 30 '26 04:08

hon2a