rounding a number in input but keep full decimal value in model - angularjs

I hope that title made sense. Basically I have an small app that does various social security calculations. A user can enter information like birth date, gender, salary etc, and click "calculate social security" and their monthly social security payouts display in an input field. The user can also, if they choose, enter that number in manually. The problem is that the value for that calculation is used elsewhere in the app, so for accuracy, i think I need the full decimal value. But cosmetically, i only need it to the dollar value (2046 vs 2046.3339228485938 bla bla bla). I've seen several tutorials on writing directives for that but that will change the value in the model. It's crossed my mind that i may be going about this the wrong way entirely but i'm turning to stackoverflow in the hopes that this is a common issue that I just cant seem to find the right words for to google.
thanks,
John

Yes, no need of custom filter for this purpose. Angular has its own filter to do this job.
Try this one
<div ng-controller="Ctrl">{{dollar | number:2}}</div>

You can create specific filter, something like:
var app = angular.module("MyApp",[]);
app.filter("rounded",function(){
return function(val,to){
return val.toFixed(to || 0);
}
});
function Ctrl($scope){
$scope.dollar=2046.3339228485938;
}
And use it like:
<div ng-controller="Ctrl">{{ dollar | rounded:2 }}</div>
Example: http://jsfiddle.net/RdgR2/

The other answers aren't quite correct for this because the OP wants it on an input field, but the rounding to be display only.
Using parsers and formatters is a working approach. I found this snippet (https://www.snip2code.com/Snippet/43223/angular-directive-to-format-a-number-in-) and modified it.
Working fiddle is here: http://jsfiddle.net/2akaxojg/
.directive('inputCurrency', function ($filter, $locale) {
return {
terminal: true,
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ngModel) {
if (!ngModel)
return; // do nothing if no ng-model
// get the number format
var formats = $locale.NUMBER_FORMATS;
// fix up the incoming number to make sure it will parse into a number correctly
var fixNumber = function (number) {
if (number) {
if (typeof number !== 'number') {
number = number.replace(',', '');
number = parseFloat(number);
}
}
return number;
}
// function to do the rounding
var roundMe = function (number) {
number = fixNumber(number);
if (number) {
return $filter('number')(number, 2);
}
}
// Listen for change events to enable binding
element.bind('blur', function () {
element.val(roundMe(ngModel.$modelValue));
});
// push a formatter so the model knows how to render
ngModel.$formatters.push(function (value) {
if (value) {
return roundMe(value);
}
});
// push a parser to remove any special rendering and make sure the inputted number is rounded
ngModel.$parsers.push(function (value) {
return value;
});
}
};
});

Angular has a native number and currency filter:
http://docs.angularjs.org/api/ng.filter:number
http://docs.angularjs.org/api/ng.filter:currency

Related

AngularJS: What filter type may I use to create custom currency directive?

I have an input form that takes currency values in the hundreds, millions, billions etc. To make it user-friendly I'd like those custom input fields to show and let the user input e.g. '1.5M' instead of 1500000, '1B' instead of 1000000000 and so on. For that purpose I created a FormatServices service with two methods currParse(value: string): number and currFormat(value: number): string and they do what they are supposed to i.e. transform between model and display values and back.
Now I define a custom currency directive as follows:
.directive("currency", function($filter, formatServices: FormatServices){
// convert to number
var p = function(viewValue){
if (angular.isDefined(viewValue)) {
return $filter('number')(formatServices.currParse(viewValue));
}
};
var f = function(modelValue){
if (angular.isDefined(modelValue)) {
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< HERE WHAT FILTER TYPE?
return $filter('???')(formatServices.currFormat(modelValue));
}
};
return {
require: 'ngModel',
link: function(scope, ele, attr, ctrl){
ctrl.$parsers.unshift(p);
ctrl.$formatters.unshift(f);
}
};
})
In the annotated line I need a filter type. Looking at the available filter types in Ng I tried with number but doesn't work because is a string output. json filter type works but the output includes double quotes around it. uppercase and lowercase don't work either because they destroy my format e.g. I can have 100k or 1.5M. There is no text filter so I don't know what to fill in there to have this working ...
You can make you own filter.
This example make an IntegerCurrency.
.filter('myCurrency', ['$filter', function ($filter) {
return function(input) {
input = parseFloat(input);
if(input % 1 === 0) {
input = input.toFixed(0);
}
return '$' + input;
//you can change $ or € put ',' '.' '-' or decorate whate ever the output.
};
}])

AngularJS smart-table search within multiple columns

Smart-table has a built in functionality to search through all columns (st-search) or through one desired column (st-search="'firstName'). Is it possible to do a search within several columns (not all)?
Example: if I have table like this: name, nickname, address with such data:
John, JJ, Some address
Steve, John, also address
Jane, Jane, John-village
and do a search for 'John' only first two columns should be as result.
Is it possible?
I have a similar problem and I solved using the hints in this post.
Smart Table doc says:
The stSetFilter replaces the filter used when searching through Smart
Table. When the default behavior for stSearch does not meet your
demands, like in a select where one entry is a substring of another,
use a custom filter to achieve your goals.
and:
Note that st-safe-src is required for the select to properly display
all distinct elements in the collection. Should this be omitted, the
select would only contain values from elements visible in table, also
affected by paging.
You can declare your own filter inside table element in HTML:
<table ... st-set-filter="myCustomFilter" class="table table-striped">
...and your can customize your filter (in your app) through a predicate function. It could work in this way:
// filter method, creating `myCustomFilter` a globally
// available filter in our module
.filter('myCustomFilter', ['$filter', function($filter) {
// function that's invoked each time Angular runs $digest()
return function(input, predicate) {
searchValue = predicate['$'];
//console.log(searchValue);
var customPredicate = function(value, index, array) {
console.log(value);
// if filter has no value, return true for each element of the input array
if (typeof searchValue === 'undefined') {
return true;
}
var p0 = value['name'].toLowerCase().indexOf(searchValue.toLowerCase());
var p1 = value['nickname'].toLowerCase().indexOf(searchValue.toLowerCase());
if (p0 > -1 || p1 > -1) {
// return true to include the row in filtered resultset
return true;
} else {
// return false to exclude the row from filtered resultset
return false;
}
}
//console.log(customPredicate);
return $filter('filter')(input, customPredicate, false);
}
}])
I made this little plnkr to see the filter in action
Nope, a workaround is to create you own directive which require the table controller and call its api twice (as the search get added)
directive('stSearch', ['$timeout', function ($timeout) {
return {
require: '^stTable',
scope: {
predicate: '=?stSearch'
},
link: function (scope, element, attr, ctrl) {
var tableCtrl = ctrl;
// view -> table state
element.bind('input', function (evt) {
evt = evt.originalEvent || evt;
tableCtrl.search(evt.target.value, 'column1');
tableCtrl.search(evt.target.value, 'column2');
});
}
};
}]);
you'll find more details in the stSearch direcitve
You can solve the issue by creating the new search enabled table and combining the fields that you might be showing in one column.
example:if you have IdNo1 and IdNo2 as view fileds in One column, you can combine them to add the new element in the table array.
View :
table injection:
table st-table="displayedCollection" st-safe-src="rowSearchCollection"
Search injection:
input type="search" ng-model="idSearch" st-search="idSearch"
Controller:
$scope.rowSearchCollection = [];
vm.searchEnabledTable = function(tableDetails) {
//$scope.rowSearchCollection = angular.copy(tableDetails);
var deferred = $q.defer();
if(_.isArray(tableDetails)) {
_.each(tableDetails, function(element, index, list) {
$scope.rowSearchCollection[index] = angular.copy(element);
$scope.rowSearchCollection[index].idSearch = element.IdNo1+','+element.IdNo2;
});
deferred.resolve("DATA_PROCESSED");
} else {
deferred.reject("NOT_ARRAY");
}
return deferred.promise;
}
the problem with stSetFilter is that the new filter will be to all the
searchs (st-search) that you will use in the table.
Another idea:
If your data rowCollection is not too big,
in the javascript in the init() of the page do something like:
self.rowCollection.forEach(function (element) {
element.NameForFilter = element.name+ ' ' + element.nickName;
});
and then in the input of the th:
st-search=NameForFilter
try st-search="{{firstName + nickname}}". I tried with smart table v2.1.6 and seems working.

angularjs with ace code editor - forcing the editor to execute 'onblur' when model changes

I am using angularjs in conjunction with ui-ace, a library that has a directive for the popular ace library.
ui-ace
ace text editor
I have made some modifications to the directive because I need it to work with string[], instead of normal strings. Everything works fine except a strange situation when switching my core model. Here is how it is set up;
There is a grid with objects from my database.
When an item in the grid is clicked, the $scope.Model is populated with the information from the database
This includes a property called Scripting, which is a string[], and it is bound to the ace text editor.
the editor's text is set to $scope.Model.Scripting.join('\n')
this behavior is repeated in different ways in the editor's onChange and onBlur events.
Now, what is going wrong is that I have to actually click on the text editor to trigger the onBlur event before I click on an item in the grid. This has to be repeated each time, or the editor won't update. I cannot figure out why this is happening.
Here is the relevant code. I am going to link the whole directive, as well. The plunkr has everything needed to reproduce the issue, including exact instructions on how to do so.
Full Demonstration and Full Directive Live (Plunkr)
Relevant Directive Changes
return {
restrict: 'EA',
require: '?ngModel',
priority: 1,
link: function (scope, elm, attrs, ngModel) {
/**
* Corresponds to the ngModel, and will enable
* support for binding to an array of strings.
*/
var lines = scope.$eval(attrs.ngModel);
/*************************************************
* normal ui-ace code
************************************************/
/**
* Listener factory. Until now only change listeners can be created.
* #type object
*/
var listenerFactory = {
/**
* Creates a blur listener which propagates the editor session
* to the callback from the user option onBlur. It might be
* exchanged during runtime, if this happens the old listener
* will be unbound.
*
* #param callback callback function defined in the user options
* #see onBlurListener
*/
onBlur: function (callback) {
return function (e) {
if (angular.isArray(lines)) {
scope.$apply(function () {
ngModel.$setViewValue(acee.getSession().doc.$lines);
});
}
executeUserCallback(callback, acee);
};
}
};
// Value Blind
if (angular.isDefined(ngModel)) {
ngModel.$formatters.push(function (value) {
if (angular.isUndefined(value) || value === null) {
return '';
}
else if (angular.isArray(value)) {
return '';
}
// removed error if the editor is bound to array
else if (angular.isObject(value)) {
throw new Error('ui-ace cannot use an object as a model');
}
return value;
});
ngModel.$render = function () {
if (angular.isArray(lines)) {
session.setValue(scope.$eval(attrs.ngModel).join('\n'));
}
else {
// normal ui-ace $render behavior
}
};
}
// set the value when the directive first runs.
if (angular.isArray(lines)) {
ngModel.$setViewValue(acee.getSession().doc.$lines);
}
Looks like you set up ngModel.$formatters incorrectly for your array case.
Try changing:
else if (angular.isArray(value)) {
return '';
}
To:
else if (angular.isArray(value)) {
return value.join('');
}
Personally I think it would have been easier to pass joined arrays in to model and not modify the directive. Then you wouldn't have issues with future upgrades
DEMO

How should I handle partial forms with angularjs?

I think someone must have run into this situation before. Basically I have a big "form" which is composed of multiple smaller "forms" inside. (In fact, they are not real forms, just sets of inputs that are grouped together to collect info for models).
This form is for a checkout page, which contains:
shipping address
shipping method
billing address
billing method
other additional info such as discounts code input, gift wrapping etc.
I would like to update the user filled info to the server as soon as they complete each part (for example, when they complete shipping address). However, I want to make it work seamlessly without the need for the users to click some kind of "update" button after filling each partial part. I wonder if there is some way to go around this?
You'll want to $watch the fields in question and act upon them (say save to db) when they are filled in. The issue you will run into is how to determine when a user has filled fields in. Things like onblur etc don't work very well in practice. I would recommend using what is called a debounce function which is basically a function that allows the user to pause for X amount of time without our code going "ok done! now let's.. ohh wait still typing..."
Here's an example that I use on my own cart - I want to automatically get shipping quotes once I have an address so I watch these fields, allow some pausing with my debounce function then call my server for quotes.
Here's some controller code:
// Debounce function to wait until user is done typing
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
// Apply debounce to our shipping rate fetch method
var fetch = debounce(function() {
$scope.fetching = true;
cartService.updateShipping($scope.shipping, function(data) {
$scope.fetching = false;
$scope.quotes = data;
});
}, 1000);
// Watch the shipping fields - when enough done and user is done typing then get quote
$scope.$watch('shipping', function(newVal, oldVal) {
// I use this to play around with what fields I actually want before I do something
var fields = ['street', 'region', 'name', 'postal', 'country', 'city'];
var valid = true;
fields.forEach(function(field) {
if (!$scope.form[field].$valid) {
valid = false;
}
});
if (valid) fetch();
}, true);
My form fields are setup like this:
<input type="text" name="street ng-model="shipping.street" required>
<input type="text" name="name" ng-model="shipping.name" required>
Notice how I make them part of a "shipping" object - that allows me to watch the shipping fields independently of others such as billing.
Note that the above is for the extreme cases such as shipping fields. For simple things such as subscribing to a newsletter if they check a box then you don't need to use the above and can simply do an ng-click="spamMe();" call in your checkbox. That function (spamMe) would be in your controller and can then call your server etc...
var spamMe = function() {
// Grab the email field that might be at top - ideally check if it's filled in but you get the idea
var email = $scope.email;
$http.post('/api/spam', ....);
}
I'd apply a $scope.$watch on each of those variables to trigger a function that checks to see if all the fields for a given section are filled out, and if so, then submit it to the server as an ajax request.
Here's my attempt at writing this:
var shippingFields = ['address', 'city', 'state', 'zip'] // etc
function submitFieldsWhenComplete(section, fields) {
fieldValues = fields.forEach(function (field) {
return $scope[section][field]
});
if (fieldValues.every()) {
// We've got all the values, submit to the server
$http.post({
url: "/your/ajax/endpoint",
data: $scope.shipping
})
}
}
shippingFields.forEach(function(field) {
$scope.$watch(function() {
return $scope['shipping'][field]
}, function(val) {
submitFieldsWhenComplete('shipping', shippingFields);
});
});

angularjs input field directive isn't clearing errors when scope changes value

I have a directive designed to impose date range restrictions on a date input field (earliest and latest). Here is the directive below, I am using the momentjs library to do my date comparison:
.directive('startDate', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
console.log(arguments);
var compareStartDates = function (value) {
var startDateCompare = moment((attrs.startDate ? scope.$eval(attrs.startDate) : '1901-01-01'), 'YYYY-MM-DD');
if (startDateCompare && startDateCompare.isValid() && value && value.match(/\d{4}\-?\d{2}\-?\d{2}/g)) {
var valueMoment = moment(value, 'YYYY-MM-DD');
if (valueMoment && valueMoment.isValid() && valueMoment < startDateCompare) {
ctrl.$setValidity('startdate', false);
ctrl.$error['startdate'] = true;
return undefined;
}
}
ctrl.$setValidity('startdate', true);
return value;
};
ctrl.$parsers.unshift(compareStartDates);
}
};
})
JSFiddle: http://jsfiddle.net/2ug4X/
Look at the fiddle above and do the following:
1) enter "A" in the text box, the pattern error triggers.
2) click the "CLICK ME" text, which updates teh value of the model on the scope, notice the error clears
3) enter "1800-01-01" in the text box, the date restriction error triggers
4) enter "2000-01-01" in the text box which is a valid date, should clear the startdate error but it doesn't. Any idea why this is happening?
I'd expect updating the ng-model bound variable like so
scope.sample.open_date = '2000-01-01';
would clear the error on the input element like the pattern error clears.
Found this after searching more on Stack: AngularJS custom validation not firing when changing the model programatically
it seems my error was not also pushing the compare function to the ctrl.$formatters like so:
ctrl.$formatters.unshift(compareStartDates);
this did the trick!

Resources