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.