一日分の仕事の後...私はそれを持っています。
上記の EDIT で述べたように、ビュー ステートはモデルの状態だけでなく、コンポーネントにフォーカスがあるかどうかにも依存するため、ここでは $formatters と $parsers は機能しません。
ngModel.$modelValue、ngModel.$viewValue、および element.val() に明示的に割り当てて、自分で状態を維持します。
これは、一部の貧しい魂を助ける場合に備えて、フルコードです。無効な入力を最新の有効な入力に戻し、値が無効な場合は Bootstrap ポップオーバーをポップアップします。
function currency($timeout) {
return {
// We will change the model via this directive
require: '?ngModel',
link: function(scope:ng.IScope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Read the options passed in the directive
var options = scope.$eval(attrs.currency);
if (options === undefined) options = {};
if (options.min === undefined) options.min = Number.NEGATIVE_INFINITY;
if (options.max === undefined) options.max = Number.POSITIVE_INFINITY;
if (options.decimals === undefined) options.decimals = 0;
if (options.decSep === undefined) options.decSep = ',';
if (options.thSep === undefined) options.thSep = '.';
// cache the validation regexp inside our options object (don't compile it all the time)
var regex = "^[0-9]*(" + options.decSep + "([0-9]{0," + options.decimals + "}))?$";
options.compiledRegEx = new RegExp(regex);
// Use a Bootstrap popover to notify the user of erroneous data
function showError(msg:string) {
if (options.promise !== undefined) {
// An error popover is already there - cancel the timer, destroy the popover
$timeout.cancel(options.promise);
element.popover('destroy');
}
// Show the error
element.popover({
animation:true, html:false, placement:'right', trigger:'manual', content:msg
}).popover('show');
// Schedule a popover destroy after 3000ms
options.promise = $timeout(function() { element.popover('destroy'); }, 3000);
}
// Converters to and from between the model (number) and the two state strings (edit/view)
function numberToEditText(n:number):string {
if (!n) return ''; // the model may be undefined by the user
return n.toString().split(localeDecSep).join(options.decSep);
}
function numberToViewText(n:number):string {
if (!n) return ''; // the model may be undefined by the user
var parts = n.toString().split(localeDecSep);
// Using SO magic: http://stackoverflow.com/questions/17294959/how-does-b-d3-d-g-work-for-adding-comma-on-numbers
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, options.thSep);
return parts.join(options.decSep);
}
function editTextToNumber(t:string):number {
return parseFloat(t.replace(options.thSep, '').replace(options.decSep, localeDecSep));
}
function viewTextToNumber(t:string):number {
return parseFloat(t.replace(options.decSep, localeDecSep));
}
// For debugging
//function log() {
// console.log('oldModelValue:' + options.oldModelValue);
// console.log('modelValue:' + ngModel.$modelValue);
// console.log('viewValue:' + ngModel.$viewValue);
//}
// On keyup, the element.val() has the input's new value -
// which may be invalid, violating our restrictions:
element.keyup(function(e) {
var newValue:string = element.val();
if (!options.compiledRegEx.test(newValue)) {
// it fails the regex, it's not valid
//console.log('This is invalid due to regex: ' + newValue);
$timeout(function() {
// schedule a call to render, to reset element.val to the last known good value
ngModel.$render(true);
}, 0);
// Show a bootstrap popever error window, which will autohide after 3 seconds
showError(' Μόνο ' + options.decimals + ' δεκαδικά και μία υποδιαστολή (' + options.decSep +')');
return;
}
var newValueNumber:number = scope.$eval(newValue.replace(options.decSep, localeDecSep));
if (newValueNumber>options.max || newValueNumber<options.min) {
// it fails the range check
//console.log('This is invalid due to range: ' + newValue);
$timeout(function() {
// schedule a call to render, to reset element.val to the last known good value
ngModel.$render(true);
}, 0);
// Show a bootstrap popever error window, which will autohide after 3 seconds
showError(' Από ' + options.min + ' έως ' + options.max);
return;
}
// The input may be empty - set the model to undefined then
// ('unset' is a valid result for our model - think of SQL 'NULL')
if (newValue === '') {
ngModel.$modelValue = undefined;
options.oldModelValue = undefined;
} else {
// The new input value is solid - update the $modelValue
ngModel.$modelValue = editTextToNumber(newValue);
// ...and keep this as the last known good value
options.oldModelValue = ngModel.$modelValue;
//console.log("oldModelValue set to " + options.oldModelValue);
}
// If we reached here and a popover is still up, waiting to be killed,
// then kill the timer and destroy the popover
if (options.promise !== undefined) {
$timeout.cancel(options.promise);
element.popover('destroy');
}
});
// schedule a call to render, to reset element.val to the last known good value
element.focus(function(e) { ngModel.$render(true); });
element.blur(function(e) { ngModel.$render(false); });
// when the model changes, Angular will call this:
ngModel.$render = (inFocus) => {
// how to obtain the first content for the oldModelValue that we will revert to
// when erroneous inputs are given in keyup() ?
// simple: just copy it here, and update in keyup if the value is valid.
options.oldModelValue = ngModel.$modelValue;
//console.log("oldModelValue set to " + options.oldModelValue);
if (!ngModel.$modelValue) {
element.val('');
} else {
// Set the $viewValue to a proper representation, based on whether
// we are in edit or view mode.
// Initially I was calling element.is(":focus") here, but this was not working
// properly - so I hack a bit: I know $render will be called by Angular
// with no parameters (so inFocus will be undefined, which evaluates to false)
// and I only call it myself with true from within 'element.focus' above.
var m2v = inFocus?numberToEditText:numberToViewText;
var viewValue = m2v(ngModel.$modelValue);
ngModel.$viewValue = viewValue;
// And set the content of the DOM element to the proper representation.
element.val(viewValue);
}
}
// we need the model of the input to update from the changes done by the user,
// but only if it is valid - otherwise, we want to use the oldModelValue
// (the last known good value).
ngModel.$parsers.push(function(newValue) {
if (newValue === '')
return undefined;
if (!options.compiledRegEx.test(newValue))
return options.oldModelValue;
var newValueNumber:number = scope.$eval(newValue.replace(options.decSep, localeDecSep));
if (newValueNumber>options.max || newValueNumber<options.min)
return options.oldModelValue;
// The input was solid, update the model.
return viewTextToNumber(newValue);
});
}
};
}