web-dev-qa-db-fra.com

Utilisation de la directive AngularJS pour formater le champ de saisie tout en laissant la variable de portée inchangée

Je rencontre un problème de formatage d'un champ de saisie tout en laissant la variable de portée sous-jacente non formatée.

Ce que je veux réaliser est un champ de texte pour afficher la devise. Il devrait se formater à la volée, tout en gérant les erreurs de saisie. Cela fonctionne, mais mon problème est que je veux stocker la valeur non formatée dans ma variable de portée. Le problème avec l'entrée est que cela nécessite un modèle qui va dans les deux sens, donc changer le champ d'entrée met à jour le modèle, et inversement.

Je suis tombé sur $parsers et $formatters qui semble être ce que je recherche. Malheureusement, ils ne s’affectent pas (ce qui peut être utile pour éviter des boucles sans fin).

J'ai créé un simple jsFiddle: http://jsfiddle.net/cruckie/yE8Yj/ et le code est le suivant:

HTML:

<div data-ng-app="app" data-ng-controller="Ctrl">
    <input type="text" data-currency="" data-ng-model="data" />
    <div>Model: {{data}}</div>
</div>

JS:

var app = angular.module("app", []);

function Ctrl($scope) {
    $scope.data = 1234567;
}

app.directive('currency', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function (scope, element, attr, ctrl) {

            ctrl.$formatters.Push(function(modelValue) {
                return modelValue.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ',');
            });

            ctrl.$parsers.Push(function(viewValue) {
                return parseFloat(viewValue.replace(new RegExp(",", "g"), ''));
            });
        }
    };
});

Encore une fois, ceci est juste un exemple simple. Quand il charge tout ressemble à ce qu'il est supposé. Le champ de saisie est formaté et la variable ne l’est pas. Cependant, lorsque vous modifiez la valeur dans le champ de saisie, il ne se formate plus lui-même - la variable est toutefois mise à jour correctement.

Existe-t-il un moyen de s'assurer que le champ de texte est formaté alors que la variable ne l'est pas? Je suppose que ce que je recherche, c’est un filtre pour les champs de texte, mais je ne vois rien trouver à ce sujet.

Meilleures salutations

22
Casper

Voici un violon qui montre comment j'ai implémenté exactement le même comportement dans mon application. J'ai fini par utiliser ngModelController#render au lieu de $formatters, puis en ajoutant un ensemble de comportements distinct qui s'est déclenché lors des événements keydown et change.

http://jsfiddle.net/KPeBD/2/

15
Wade Tandy

J'ai refactoré la directive d'origine, de sorte qu'elle utilise $ parses et $ formateurs au lieu d'écouter les événements du clavier. Il n’est pas non plus nécessaire d’utiliser $ browser.defer

Voir la démo de travail ici http://jsfiddle.net/davidvotrubec/ebuqo6Lm/

    var myApp = angular.module('myApp', []);

    myApp.controller('MyCtrl', function($scope) {
      $scope.numericValue = 12345678;
    });

    //Written by David Votrubec from ST-Software.com
    //Inspired by http://jsfiddle.net/KPeBD/2/
    myApp.directive('sgNumberInput', ['$filter', '$locale', function ($filter, $locale) {
            return {
                require: 'ngModel',
                restrict: "A",
                link: function ($scope, element, attrs, ctrl) {
                    var fractionSize = parseInt(attrs['fractionSize']) || 0;
                    var numberFilter = $filter('number');
                    //format the view value
                    ctrl.$formatters.Push(function (modelValue) {
                        var retVal = numberFilter(modelValue, fractionSize);
                        var isValid = isNaN(modelValue) == false;
                        ctrl.$setValidity(attrs.name, isValid);
                        return retVal;
                    });
                    //parse user's input
                    ctrl.$parsers.Push(function (viewValue) {
                        var caretPosition = getCaretPosition(element[0]), nonNumericCount = countNonNumericChars(viewValue);
                        viewValue = viewValue || '';
                        //Replace all possible group separators
                        var trimmedValue = viewValue.trim().replace(/,/g, '').replace(/`/g, '').replace(/'/g, '').replace(/\u00a0/g, '').replace(/ /g, '');
                        //If numericValue contains more decimal places than is allowed by fractionSize, then numberFilter would round the value up
                        //Thus 123.109 would become 123.11
                        //We do not want that, therefore I strip the extra decimal numbers
                        var separator = $locale.NUMBER_FORMATS.DECIMAL_SEP;
                        var arr = trimmedValue.split(separator);
                        var decimalPlaces = arr[1];
                        if (decimalPlaces != null && decimalPlaces.length > fractionSize) {
                            //Trim extra decimal places
                            decimalPlaces = decimalPlaces.substring(0, fractionSize);
                            trimmedValue = arr[0] + separator + decimalPlaces;
                        }
                        var numericValue = parseFloat(trimmedValue);
                        var isEmpty = numericValue == null || viewValue.trim() === "";
                        var isRequired = attrs.required || false;
                        var isValid = true;
                        if (isEmpty && isRequired) {
                            isValid = false;
                        }
                        if (isEmpty == false && isNaN(numericValue)) {
                            isValid = false;
                        }
                        ctrl.$setValidity(attrs.name, isValid);
                        if (isNaN(numericValue) == false && isValid) {
                            var newViewValue = numberFilter(numericValue, fractionSize);
                            element.val(newViewValue);
                            var newNonNumbericCount = countNonNumericChars(newViewValue);
                            var diff = newNonNumbericCount - nonNumericCount;
                            var newCaretPosition = caretPosition + diff;
                            if (nonNumericCount == 0 && newCaretPosition > 0) {
                                newCaretPosition--;
                            }
                            setCaretPosition(element[0], newCaretPosition);
                        }
                        return isNaN(numericValue) == false ? numericValue : null;
                    });
                } //end of link function
            };
            //#region helper methods
            function getCaretPosition(inputField) {
                // Initialize
                var position = 0;
                // IE Support
                if (document.selection) {
                    inputField.focus();
                    // To get cursor position, get empty selection range
                    var emptySelection = document.selection.createRange();
                    // Move selection start to 0 position
                    emptySelection.moveStart('character', -inputField.value.length);
                    // The caret position is selection length
                    position = emptySelection.text.length;
                }
                else if (inputField.selectionStart || inputField.selectionStart == 0) {
                    position = inputField.selectionStart;
                }
                return position;
            }
            function setCaretPosition(inputElement, position) {
                if (inputElement.createTextRange) {
                    var range = inputElement.createTextRange();
                    range.move('character', position);
                    range.select();
                }
                else {
                    if (inputElement.selectionStart) {
                        inputElement.focus();
                        inputElement.setSelectionRange(position, position);
                    }
                    else {
                        inputElement.focus();
                    }
                }
            }
            function countNonNumericChars(value) {
                return (value.match(/[^a-z0-9]/gi) || []).length;
            }
            //#endregion helper methods
        }]);

Le code Github est ici [ https://github.com/ST-Software/STAngular/blob/master/src/directives/SgNumberInput]

5
David Votrubec

J'ai révisé un peu ce que Wade Tandy avait fait et ajouté un support pour plusieurs fonctionnalités:

  1. le séparateur de milliers est pris à partir de $ locale
  2. le nombre de chiffres après le signe décimal est pris par défaut à partir de $ locale et peut être remplacé par l'attribut fraction
  3. l'analyseur n'est activé que sur les modifications, et non sur les raccourcis clavier, couper et coller, pour éviter d'envoyer le curseur à la fin de l'entrée à chaque modification
  4. Les touches Accueil et Fin sont également autorisées (pour sélectionner le texte entier à l'aide du clavier)
  5. définir la validité à false lorsque l'entrée n'est pas numérique, cela se fait dans l'analyseur:

            // This runs when we update the text field
        ngModelCtrl.$parsers.Push(function(viewValue) {
            var newVal = viewValue.replace(replaceRegex, '');
            var newValAsNumber = newVal * 1;
    
            // check if new value is numeric, and set control validity
            if (isNaN(newValAsNumber)){
                ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', false);
            }
            else{
                newVal = newValAsNumber.toFixed(fraction);
                ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', true);
            }
            return newVal;
    
        });
    

Vous pouvez voir ma version révisée ici - http://jsfiddle.net/KPeBD/64/

5
Lior Chaga

En effet, le $parsers et le $formatters sont "indépendants" comme vous dites (probablement pour des boucles, encore une fois comme vous le dites). Dans notre application, nous formatons explicitement avec l'événement onchange (dans la fonction link), à peu près comme:

element.bind("change", function() {
    ...
    var formattedModel = format(ctrl.$modelValue);
    ...
    element.val(formattedModel);
});

Voir votre violon mis à jour pour l'exemple détaillé et fonctionnel: http://jsfiddle.net/yE8Yj/1/

J'aime relier l'événement onchange, car je trouve ennuyeux de modifier l'entrée pendant que l'utilisateur tape.

3

Basé sur la réponse de Wade Tandy, voici un nouveau jsfiddle avec les améliorations suivantes:

  • nombres décimaux possibles 
  • séparateur de milliers et séparateur décimal basé sur les sections locales 
  • et quelques autres réglages ...

J'ai également remplacé tout String.replace(regex) par split().join(), car cela me permet d'utiliser des variables dans l'expression.

http://jsfiddle.net/KPeBD/283/

0
Gerfried

Le violon utilise une ancienne version de angular (1.0.7).

Lors de la mise à jour vers une version récente, 1.2.6, la fonction $ render de ngModelCtrl n'est jamais appelée, ce qui signifie que si la valeur du modèle est modifiée dans le contrôleur,

le numéro n'est jamais formaté comme requis dans la vue.

//check if new value is numeric, and set control validity
if (isNaN(newValAsNumber)){
  ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', false);
}

Voici le violon mis à jour http://jsfiddle.net/KPeBD/78/

0
oderok