Skip to content

Instantly share code, notes, and snippets.

@marius7383
Forked from jakiestfu/ko-money.js
Last active April 26, 2016 10:33
Show Gist options
  • Save marius7383/5bd1db68e1056af2cdc651e58640cbac to your computer and use it in GitHub Desktop.
Save marius7383/5bd1db68e1056af2cdc651e58640cbac to your computer and use it in GitHub Desktop.
Used to display and read formatted money via Knockout binding
(function () {
// Credit for escapeRegExp goes to MDN
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters
function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
// Credit for replaceAll goes to Sean Bright
// http://stackoverflow.com/a/1144788/2919694
function replaceAll(str, find, replace) {
return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
}
var toMoney = function (num, decimalSeparator, thousandsSeparator, currency, leadingCurrency) {
if ((num != 0 && !num) || isNaN(num)) {
return num; // prevent data loss if invalid input is provided
}
if (decimalSeparator == thousandsSeparator) {
throw "Decimal separator must not be the same string as thousands separator";
}
var output = '';
if (currency && leadingCurrency) {
output = currency;
}
if (typeof num === 'string') {
num = parseFloat(num);
}
var fixed = num.toFixed(2);
// as long as toLocaleString is not properly defined (ES5 15.7.4.3) or
// ECMA-402 not properly implemented in all major browsers (Safari is missing, customers require < IE11),
// we have to do it ourselves
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString#Browser_compatibility
fixed = (fixed.replace(/(\d)(?=(\d{3})+\.)/g, '$1' + thousandsSeparator));
// the following works under the condition that toFixed(2) provides two decimals
// get part in front of decimal separator
var thousands = fixed.slice(0, -3);
// get poart after decimal separator
var decimals = fixed.substr(-2, 2);
output += (thousands + decimalSeparator + decimals);
if (currency && !leadingCurrency) {
output += currency;
}
return output;
};
var fromMoney = function (num, decimalSeparator, thousandsSeparator, currency, leadingCurrency) {
if (!num) {
return num; // prevent data loss if invalid input is provided
}
if (decimalSeparator == thousandsSeparator) {
throw "Decimal separator must not be the same string as thousands separator";
}
if (typeof num !== 'string') {
num = num.toString();
}
if (currency) {
if (leadingCurrency) {
num = num.substr(currency.length);
} else if (!leadingCurrency) {
num = num.slice(0, -1 * currency.length);
}
}
// remove thousands separators and replace decimal separator by dot
num = replaceAll(num, thousandsSeparator, "");
num = replaceAll(num, decimalSeparator, ".");
if (isNaN(num)) {
return num; // prevent data loss if invalid input is provided
}
num = parseFloat(num);
return num;
};
var subscribe = function (valueAccessor, callback) {
var config = valueAccessor();
var members = ['decimalSeparator', 'thousandsSeparator', 'currency', 'leadingCurrency'];
for (var i = 0; i < members.length; i++) {
var observable = config[members[i]];
if (ko.isObservable(observable)) {
observable.subscribe(callback);
}
}
};
var process = function (valueAccessor, value, parse) {
var config = valueAccessor();
value = value || ko.unwrap(config.value);
var decimalSeparator = ko.unwrap(config.decimalSeparator) || '.';
var thousandsSeparator = ko.unwrap(config.thousandsSeparator) || ',';
var currency = ko.unwrap(config.currency) || '';
var leadingCurrency = ko.unwrap(config.sourceFormat) || false;
var text = null;
if (!parse) {
text = toMoney(value, decimalSeparator, thousandsSeparator, currency, leadingCurrency);
} else {
text = fromMoney(value, decimalSeparator, thousandsSeparator, currency, leadingCurrency);
}
return text;
};
var readonlyUpdate = function (element, valueAccessor) {
var text = process(valueAccessor, null, false);
ko.utils.setTextContent(element, text);
};
var valueUpdate = function (element, valueAccessor) {
var text = process(valueAccessor, null, false);
$(element).val(text);
};
/**
* Binding handler that formats the output value of a given observable as money.
*
* Structure of binding value:
* money: {
* value: the observable containing the value
* decimalSeparator: can be observable or constant string, defaults to dot '.'
* thousandsSeparator: can be observable or constant string, defaults to comma ','
* currency: the currency sign like $ or €, which is either appended or prepended; can be observable or constant string, defaults to empty string ('')
* leadingCurrency: determines if the currency sign should be prepended (= true) or appended (=false); can be observable or constant string, defaults to false;
* }
*/
ko.bindingHandlers.money = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
subscribe(valueAccessor, readonlyUpdate.bind(this, element, valueAccessor));
return {'controlsDescendantBindings': true};
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
readonlyUpdate(element, valueAccessor);
}
};
/**
* Binding handler that formats the output value of a given observable as money into a text box.
* Changes to the input are parsed to float and given back to the observable.
* If the input is erroneous, the unparsed input is given back to the observable to prevent data loss.
*
* Structure of binding value:
* money: {
* value: the observable containing the value
* decimalSeparator: can be observable or constant string, defaults to dot '.'
* thousandsSeparator: can be observable or constant string, defaults to comma ','
* currency: the currency sign like $ or €, which is either appended or prepended; can be observable or constant string, defaults to empty string ('')
* leadingCurrency: determines if the currency sign should be prepended (= true) or appended (=false); can be observable or constant string, defaults to false;
* }
*/
ko.bindingHandlers.moneyValue = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
subscribe(valueAccessor, valueUpdate.bind(this, element, valueAccessor));
$(element).change(function (e) {
var config = valueAccessor();
var $this = $(this);
var value = $this.val();
value = process(valueAccessor, value, true);
config.value(value);
});
return {'controlsDescendantBindings': true};
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
valueUpdate(element, valueAccessor);
}
};
})();
<script type="text/javascript">
ko.applyBindings({
value: ko.observable(2500000.00),
currency: ko.observable('€'),
leadingCurrency: ko.observable(false),
decimalSeparator: ko.observable(','),
thousandsSeparator: ko.observable('.')
});
</script>
<!-- Sets span's text to 2.500.000,00€ but keeps observable at 250000.00 -->
<span data-bind="money: { value: value, currency: currency, leadingCurrency: leadingCurrency, decimalSeparator: decimalSeparator, thousandsSeparator: thousandsSeparator }"></span>
<!-- Sets textbox value to 2.500.000,00€ but keeps observable at 250000.00 -->
<input type="text" data-bind="moneyValue: { value: value, currency: currency, leadingCurrency: leadingCurrency, decimalSeparator: decimalSeparator, thousandsSeparator: thousandsSeparator }">
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment