0

I want to create a directive that will behave as follows... On the HTML side:

<input data-ng-model="modelLogistics.inputValue1" 
       data-currency="{decSep:','   ,    thSep:'.'}">

Angular-wise, on the user side of code we would have something like:

controllerLogistics(...) {
    $scope.modelLogistics = {};
    $scope.modelLogistics.inputValue1 = 1234.23;
    ...
}

Now for the tough part: I want the input control to behave in two ways, depending on whether it has the focus or not:

  • If the control has the focus, then it should display the number using only the decimal separator (decSep) and ignoring the thousand separator (thSep) - so the 1234.23 would appear in the input text that the user edits as "1234,23" (because decSep is set to ',' in the HTML directive).
  • If the control loses the focus, then it should display the number using both the decimal separator (decSep) and the thousand separator (thSep) - so the 1234.23 would appear in the input text that the user sees as "1.234,23" (thSep is set to '.' in the HTML directive).

My code so far is this:

function currency() {
    return {
        require: '?ngModel',
        link: function(scope:ng.IScope, element, attrs, ngModel) {
            if(!ngModel) return; // do nothing if no ng-model

            var options = scope.$eval(attrs.currency);
            if (options === undefined)          options = {};
            if (options.decSep === undefined)   options.decSep = ',';
            if (options.thSep === undefined)    options.thSep = '.';

            element.blur(function(e) {
                var parts = (ngModel.$viewValue || '').split(options.decSep);
                parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, options.thSep);
                element.val( parts.join(options.decSep));
            });

            ngModel.$render = () => {
                element.val(ngModel.$viewValue || '');
            }
       }
  }

...and it works - provided that (a) my model is a string, not a number, and (b) I initialize the model with a "valid" number as per the directive specs in the HTML - that is, using model values like "1234,23" and not the number 1234.23

I am having difficulty figuring out how to change the implementation to have an underlying number (not a string) and automatically using the two modes (edit/view). I have seen the angular filters (i.e. the '|' syntax in things like '{{model.value | something}}' but I am not sure whether it fits with what I am trying to do...

Any help most appreciated.

EDIT

I have seen other currency solutions that use $formatters and $parsers - but in my case, I can't use this pattern, because the $viewValue depends not just on the $modelValue, but also on whether the control has the focus or not. That is, if I just add a formatter that checks whether the element is in focus or not, that will work the first time only - when the user clicks on some other component and the focus is lost, the model hasn't changed - yet the view needs to be updated.

4

1 回答 1

0

经过一天的工作......我有它。

正如我在上面的 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);
            });
        }
    };
}
于 2013-10-02T14:51:50.453 回答