web-dev-qa-db-fra.com

Comment implémenter un ng-change pour une directive personnalisée

J'ai une directive avec un modèle comme

<div>
    <div ng-repeat="item in items" ng-click="updateModel(item)">
<div>

Ma directive est déclarée comme:

return {
    templateUrl: '...',
    restrict: 'E',
    require: '^ngModel',
    scope: {
        items: '=',
        ngModel: '=',
        ngChange: '&'
    },
    link: function postLink(scope, element, attrs) 
    {
        scope.updateModel = function(item)
        {
             scope.ngModel = item;
             scope.ngChange();
        }
    }
}

Je voudrais avoir ng-change appelé lorsque vous cliquez sur un élément et que la valeur de foo a déjà été modifiée.

Autrement dit, si ma directive est mise en œuvre comme:

<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>

Je m'attendrais à appeler bar lorsque la valeur de foo aura été mise à jour.

Avec le code donné ci-dessus, ngChange est appelé avec succès, mais avec l'ancienne valeur de foo à la place de la nouvelle valeur mise à jour.

Une façon de résoudre le problème consiste à appeler ngChange dans un délai d’exécution afin de l’exécuter ultérieurement, lorsque la valeur de foo aura déjà été modifiée. Mais cette solution me fait perdre le contrôle de l'ordre dans lequel les choses sont censées être exécutées et je suppose qu'il devrait y avoir une solution plus élégante.

Je pourrais aussi utiliser un observateur sur foo dans la portée parente, mais cette solution ne donne pas vraiment une méthode ngChange à implémenter et on m'a dit que les observateurs sont d'excellents consommateurs de mémoire.

Existe-t-il un moyen de faire exécuter ngChange de manière synchrone sans délai d'attente ni observateur?

Exemple: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview

51
htellez

Si vous avez besoin de ngModel, vous pouvez simplement appeler $setViewValue sur ngModelController, qui évalue implicitement ng-change. Le quatrième paramètre de la fonction de liaison devrait être ngModelCtrl. Le code suivant fera ng-change travaillez pour votre directive.

link : function(scope, element, attrs, ngModelCtrl){
    scope.updateModel = function(item) {
        ngModelCtrl.$setViewValue(item);
    }
}

Pour que votre solution fonctionne, supprimez ngChange et ngModel de la portée isolée de myDirective.

En voici un exemple: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview

58
Samuli Ulmanen

tl; dr

D'après mon expérience, il vous suffit d'hériter de ngModelCtrl . l'expression ng-change sera automatiquement évaluée lorsque vous utiliserez la méthode ngModelCtrl.$setViewValue

angular.module("myApp").directive("myDirective", function(){
  return {
    require:"^ngModel", // this is important, 
    scope:{
      ... // put the variables you need here but DO NOT have a variable named ngModel or ngChange 
    }, 
    link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
      scope.setValue = function(value){
        ctrl.$setViewValue(value); // this line will automatically eval your ng-change
      };
    }
  };
});

Plus précisément

ng-change Est évalué pendant la ngModelCtrl.$commitViewValue() [~ # ~] si [~ # ~] la référence de l'objet de votre ngModel a changé. la méthode $commitViewValue() est appelée automatiquement par $setViewValue(value, trigger) si vous n'utilisez pas l'argument de déclencheur ou si vous n'avez pas précisé de ngModelOptions .

J'ai précisé que le ng-chage Serait automatiquement déclenché si la référence du $viewValue Était modifiée. Lorsque votre ngModel est un string ou un int, vous n'avez pas à vous en préoccuper. Si votre ngModel est un objet et que vous ne modifiez que certaines de ses propriétés, alors $setViewValue N'évalue pas ngChange.

Si nous prenons l'exemple de code depuis le début de l'article

scope.setValue = function(value){
    ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
    var vv = ctrl.$viewValue;
    vv.prop1 = prop1Value;
    ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
14
lucienBertin

Après quelques recherches, il semble que la meilleure approche consiste à utiliser $timeout(callback, 0).

Il lance automatiquement un $digest cycle juste après l'exécution du rappel.

Donc, dans mon cas, la solution consistait à utiliser

$timeout(scope.ngChange, 0);

Ainsi, peu importe la signature de votre rappel, celui-ci sera exécuté exactement comme vous l'avez défini dans la portée parente.

Voici le plunkr avec de tels changements: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview

9
htellez

Les réponses de Samuli Ulmanen et de lucienBertin le montrent bien, bien qu’un peu plus loin, dans la documentation d’AngularJS, donne des conseils supplémentaires sur la façon de gérer cela (voir https://docs.angularjs.org/api/ng/type/ngModel). NgModelController ).

Spécifiquement dans les cas où vous passez des objets à $ setViewValue (myObj). AngularJS Documentatation déclare:

Lorsqu'elle est utilisée avec des entrées standard, la valeur de vue sera toujours une chaîne (qui est dans certains cas analysée dans un autre type, tel qu'un objet Date pour input [date].) Toutefois, les contrôles personnalisés peuvent également transmettre des objets à cette méthode. Dans ce cas, nous devrions faire une copie de l'objet avant de le passer à $ setViewValue. En effet, ngModel n'effectue pas une surveillance approfondie des objets, il recherche uniquement un changement d'identité. Si vous ne modifiez que la propriété de l'objet, ngModel ne réalisera pas que l'objet a changé et n'appellera pas les pipelines $ Parsers et $ validators. Pour cette raison, vous ne devriez pas modifier les propriétés de la copie une fois qu'elle a été passée à $ setViewValue. Sinon, la valeur du modèle sur l'étendue risque de ne pas être modifiée correctement.

Dans mon cas particulier, mon modèle est un objet date/moment. Je dois donc tout d'abord cloner l'objet avant d'appeler setViewValue. J'ai de la chance ici car moment fournit une méthode de clonage simple: var b = moment(a);

link : function(scope, elements, attrs, ctrl) {
    scope.updateModel = function (value) {
        if (ctrl.$viewValue == value) {
            var copyOfObject = moment(value);
            ctrl.$setViewValue(copyOfObject);
        }
        else
        {
            ctrl.$setViewValue(value);
        }
    };
}
0
Arkiliknam