Comment faire un filtrage bidirectionnel dans AngularJS?

124

L'une des choses intéressantes qu'AngularJS peut faire est d'appliquer un filtre à une expression de liaison de données particulière, ce qui est un moyen pratique d'appliquer, par exemple, la mise en forme de la devise ou de la date spécifique à la culture des propriétés d'un modèle. Il est également agréable d'avoir des propriétés calculées sur la portée. Le problème est qu'aucune de ces fonctionnalités ne fonctionne avec des scénarios de liaison de données bidirectionnelle - uniquement une liaison de données unidirectionnelle de la portée à la vue. Cela semble être une omission flagrante dans une bibliothèque par ailleurs excellente - ou est-ce que je manque quelque chose?

Dans KnockoutJS , je pourrais créer une propriété calculée en lecture / écriture, ce qui me permettait de spécifier une paire de fonctions, une qui est appelée pour obtenir la valeur de la propriété et une qui est appelée lorsque la propriété est définie. Cela m'a permis d'implémenter, par exemple, une entrée tenant compte de la culture - laisser l'utilisateur taper "1,24 $" et analyser cela dans un flottant dans le ViewModel, et faire refléter les modifications dans le ViewModel dans l'entrée.

La chose la plus proche que je pourrais trouver semblable à ceci est l'utilisation de $scope.$watch(propertyName, functionOrNGExpression);ceci me permet d'avoir une fonction appelée quand une propriété dans les $scopechangements. Mais cela ne résout pas, par exemple, le problème d'entrée sensible à la culture. Notez les problèmes lorsque j'essaye de modifier la $watchedpropriété dans la $watchméthode elle-même:

$scope.$watch("property", function (newValue, oldValue) {
    $scope.outputMessage = "oldValue: " + oldValue + " newValue: " + newValue;
    $scope.property = Globalize.parseFloat(newValue);
});

( http://jsfiddle.net/gyZH8/2/ )

L'élément d'entrée devient très confus lorsque l'utilisateur commence à taper. Je l'ai amélioré en divisant la propriété en deux propriétés, une pour la valeur non analysée et une pour la valeur analysée:

$scope.visibleProperty= 0.0;
$scope.hiddenProperty = 0.0;
$scope.$watch("visibleProperty", function (newValue, oldValue) {
    $scope.outputMessage = "oldValue: " + oldValue + " newValue: " + newValue;
    $scope.hiddenProperty = Globalize.parseFloat(newValue);
});

( http://jsfiddle.net/XkPNv/1/ )

C'était une amélioration par rapport à la première version, mais c'est un peu plus verbeux, et notez qu'il y a toujours un problème de parsedValuepropriété des changements de portée (tapez quelque chose dans la deuxième entrée, ce qui change parsedValuedirectement. Notez que l'entrée supérieure ne mettre à jour). Cela peut se produire à partir d'une action du contrôleur ou du chargement de données à partir d'un service de données.

Existe-t-il un moyen plus simple d'implémenter ce scénario en utilisant AngularJS? Est-ce que je manque une fonctionnalité dans la documentation?

Jeremy Bell
la source

Réponses:

231

Il s'avère qu'il existe une solution très élégante à cela, mais ce n'est pas bien documenté.

Le formatage des valeurs de modèle pour l'affichage peut être géré par l' |opérateur et un angle formatter. Il s'avère que le ngModel qui a non seulement une liste de formateurs mais aussi une liste d'analyseurs.

1. Utilisez ng-modelpour créer la liaison de données bidirectionnelle

<input type="text" ng-model="foo.bar"></input>

2. Créez une directive dans votre module angulaire qui sera appliquée au même élément et qui dépend du ngModelcontrôleur

module.directive('lowercase', function() {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function(scope, element, attr, ngModel) {
            ...
        }
    };
});

3. Dans la linkméthode, ajoutez vos convertisseurs personnalisés au ngModelcontrôleur

function fromUser(text) {
    return (text || '').toUpperCase();
}

function toUser(text) {
    return (text || '').toLowerCase();
}
ngModel.$parsers.push(fromUser);
ngModel.$formatters.push(toUser);

4. Ajoutez votre nouvelle directive au même élément qui a déjà le ngModel

<input type="text" lowercase ng-model="foo.bar"></input>

Voici un exemple de travail qui transforme le texte en minuscules dans le inputet de nouveau en majuscules dans le modèle

La documentation de l'API pour le contrôleur de modèle contient également une brève explication et un aperçu des autres méthodes disponibles.

phaas
la source
Y a-t-il une raison pour laquelle vous avez utilisé "ngModel" comme nom du quatrième paramètre dans votre fonction de liaison? N'est-ce pas juste un contrôleur générique pour la directive qui n'a fondamentalement rien à voir avec l'attribut ngModel? (J'apprends toujours angulaire ici, donc je pourrais me tromper totalement.)
Drew Miller
7
En raison de "require: 'ngModel'", le 4ème paramètre de la fonction de liaison sera le contrôleur de la directive ngModel - c'est-à-dire le contrôleur de foo.bar, qui est une instance de ngModelController . Vous pouvez nommer le 4ème paramètre comme vous le souhaitez. (Je l'appellerais ngModelCtrl.)
Mark Rajcok
8
Cette technique est documentée sur docs.angularjs.org/guide/forms , dans la section Validation personnalisée.
Nikhil Dabas
1
@Mark Rajcok dans le violon fourni, tout en cliquant sur Charger les données - tout en minuscules, je m'attendais à ce que la valeur du modèle soit en MAJUSCULES, mais la valeur du modèle était petite. Pourriez-vous pls. expliquer pourquoi et comment rendre le modèle toujours EN MAJUSCULES
Rajkamal Subramanian
1
@rajkamal, puisque loadData2 () modifie $scopedirectement, c'est ce sur quoi le modèle sera défini ... jusqu'à ce que l'utilisateur interagisse avec la zone de texte. À ce stade, tous les analyseurs peuvent alors affecter la valeur du modèle. En plus d'un analyseur, vous pouvez ajouter une $ watch à votre contrôleur pour transformer la valeur du modèle.
Mark Rajcok