Appeler une fonction de contrôleur à partir d'une directive sans portée isolée dans AngularJS

95

Je n'arrive pas à trouver un moyen d'appeler une fonction sur la portée parent à partir d'une directive sans utiliser la portée isolée. Je sais que si j'utilise une portée isolée, je peux simplement utiliser "&" dans la portée isolée pour accéder à la fonction sur la portée parente, mais l'utilisation de la portée isolée quand ce n'est pas nécessaire a des conséquences. Considérez le code HTML suivant:

<button ng-hide="hideButton()" confirm="Are you sure?" confirm-action="doIt()">Do It</button>

Dans cet exemple simple, je veux afficher une boîte de dialogue de confirmation JavaScript et n'appeler doIt () que s'ils cliquent sur "OK" dans la boîte de dialogue de confirmation. C'est simple en utilisant une portée isolée. La directive ressemblerait à ceci:

.directive('confirm', function () {
    return {
        restrict: 'A',
        scope: {
            confirm: '@',
            confirmAction: '&'
        },
        link: function (scope, element, attrs) {
            element.bind('click', function (e) {
                if (confirm(scope.confirm)) {
                    scope.confirmAction();
                }
            });
        }
    };
})

Mais le problème est que, parce que j'utilise une portée isolée, ng-hide dans l'exemple ci-dessus ne s'exécute plus contre la portée parente , mais plutôt dans la portée isolée (car l'utilisation d'une portée isolée sur n'importe quelle directive entraîne toutes les directives sur cet élément à utilisez la portée isolée). Voici un jsFiddle de l'exemple ci-dessus où ng-hide ne fonctionne pas. (Notez que dans ce violon, le bouton doit se cacher lorsque vous tapez "oui" dans la zone de saisie.)

L'alternative serait de NE PAS utiliser un champ d'application isolé , ce que je souhaite vraiment ici, car il n'est pas nécessaire que le champ d'application de cette directive soit isolé. Le seul problème que j'ai est, comment appeler une méthode sur la portée parent si je ne la transmets pas sur une portée isolée ?

Voici un jsfiddle où je n'utilise PAS de portée isolée et le ng-hide fonctionne bien, mais, bien sûr, l'appel à confirmAction () ne fonctionne pas, et je ne sais pas comment le faire fonctionner.

Veuillez noter que la réponse que je recherche vraiment est de savoir comment appeler des fonctions sur la portée externe SANS utiliser une portée isolée. Et je ne suis pas intéressé à faire fonctionner ce dialogue de confirmation d'une autre manière, car le but de cette question est de comprendre comment faire des appels à la portée externe et toujours pouvoir faire fonctionner d'autres directives contre la portée parente.

Sinon, je serais intéressé d'entendre des solutions qui utilisent une portée isolée si d'autres directives fonctionnent toujours contre la portée parente , mais je ne pense pas que cela soit possible.

Jim Cooper
la source
Pour ceux qui passent par là, Dan Wahlin a écrit un très bon article expliquant les paramètres de portée et de fonction d'isolat: weblogs.asp.net/dwahlin/…
tanguy_k

Réponses:

117

Puisque la directive n'appelle qu'une fonction (et n'essaye pas de définir une valeur sur une propriété), vous pouvez utiliser $ eval au lieu de $ parse (avec une portée non isolée):

scope.$apply(function() {
    scope.$eval(attrs.confirmAction);
});

Ou mieux, utilisez simplement $ apply , ce qui permettra à $ eval () de comparer son argument à la portée:

scope.$apply(attrs.confirmAction);

Violon

Mark Rajcok
la source
Je ne savais pas cela, merci pour cela. J'ai toujours pensé que la méthode $ parse était un peu trop verbeuse.
Clark Pan
2
comment définiriez-vous les paramètres de cette fonction?
CMCDragonkai
2
@CMCDragonkai, si la fonction a des arguments, vous pouvez les spécifier dans le HTML,, confirm-action="doIt(arg1)"puis les définir scope.arg1avant d'appeler $ eval. Cependant, il serait probablement plus propre d'utiliser $ parse: stackoverflow.com/a/16200618/215945
Mark Rajcok
N'est-il pas possible de faire scope. $ Eval (attrs.doIt ({arg1: 'blah'}) ;?
CMCDragonkai
3
@CMCDragonkai, non, scope.$eval(attrs.confirmAction({arg1: 'blah'})ne fonctionnera pas. Vous devrez utiliser $ parse avec cette syntaxe.
Mark Rajcok
17

Vous souhaitez utiliser le service $ parse dans AngularJS.

Donc pour votre exemple:

.directive('confirm', function ($parse) {
    return {
        restrict: 'A',

        // Child scope, not isolated
        scope : true,
        link: function (scope, element, attrs) {
            element.bind('click', function (e) {
                if (confirm(attrs.confirm)) {
                    scope.$apply(function(){

                        // $parse returns a getter function to be 
                        // executed against an object
                        var fn = $parse(attrs.confirmAction);

                        // In our case, we want to execute the statement in 
                        // confirmAction i.e. 'doIt()' against the scope 
                        // which this directive is bound to, because the 
                        // scope is a child scope, not an isolated scope. 
                        // The prototypical inheritance of scopes will mean 
                        // that it will eventually find a 'doIt' function 
                        // bound against a parent's scope.
                        fn(scope, {$event : e});
                    });
                }
            });
        }
    };
});

Il y a un problème avec la définition d'une propriété à l'aide de $ parse, mais je vous laisserai traverser ce pont lorsque vous y arriverez.

Clark Pan
la source
Merci, Clark, c'est exactement ce que je cherchais. J'avais essayé d'utiliser $ parse, mais je n'avais pas réussi à passer la portée, donc cela ne fonctionnait pas pour moi. C'est parfait.
Jim Cooper du
J'ai été surpris de constater que cette solution fonctionne également sans portée: vrai. Ma compréhension était cette portée: c'est vrai ce qui fait que la portée de la directive hérite prototypiquement de la portée parente. Une idée pourquoi cela fonctionne toujours sans cela?
Jim Cooper
2
aucune portée enfant (suppression scope:true) ne fonctionne car la directive ne créera pas du tout de nouvelle portée, et donc la scopepropriété que vous référencez dans la linkfonction est exactement la même portée que la portée «parent» précédemment.
Clark Pan
$ apply suffit pour ce cas (voir ma réponse).
Mark Rajcok du
1
À quoi sert l'événement $?
CMCDragonkai
12

Appelez explicitement hideButtonla portée parent.

Voici le violon: http://jsfiddle.net/pXej2/5/

Et voici le HTML mis à jour:

<div ng-app="myModule" ng-controller="myController">
    <input ng-model="showIt"></input>
    <button ng-hide="$parent.hideButton()" confirm="Are you sure?" confirm-action="doIt()">Do It</button>
</div>
Jason
la source
+1 pour une solution de travail. J'avais en fait essayé d'utiliser $ parent et quand cela ne fonctionnait pas, j'ai abandonné. Après avoir vu votre solution, j'ai examiné de plus près et il s'est avéré que je n'avais pas ajouté $ parent aux paramètres que je transmettais (non affichés dans mon violon d'origine). Par exemple, si je voulais passer showIt à la fonction hideButton (), je devais utiliser $ parent.hideButton ($ parent.showIt) et il me manquait le deuxième $ parent. Je vais cependant accepter la réponse de Clark, car cette solution n'exige pas que l'utilisateur de ma directive sache qu'il doit ajouter $ parent. Merci pour la perspicacité!
Jim Cooper
1

Le seul problème que j'ai est, comment appeler une méthode sur la portée parent si je ne la transmets pas sur une portée isolée?

Voici un jsfiddle où je n'utilise PAS de portée isolée et le ng-hide fonctionne bien, mais, bien sûr, l'appel à confirmAction () ne fonctionne pas et je ne sais pas comment le faire fonctionner.

Tout d'abord, un petit point: dans ce cas, le champ d'application du contrôleur externe n'est pas le champ d'application parent du champ d'application de la directive; le champ d'application du contrôleur externe est le champ d'application de la directive. En d'autres termes, les noms de variables utilisés dans la directive seront recherchés directement dans la portée du contrôleur.

Ensuite, l'écriture attrs.confirmAction()ne fonctionne pas car attrs.confirmActionpour ce bouton,

<button ... confirm-action="doIt()">Do It</button>

est la chaîne "doIt()", et vous ne pouvez pas appeler une chaîne, par exemple "doIt()"().

Votre question est vraiment:

Comment appeler une fonction lorsque j'ai son nom sous forme de chaîne?

En JavaScript, vous appelez principalement des fonctions en utilisant la notation par points, comme ceci:

some_obj.greet()

Mais vous pouvez également utiliser la notation de tableau:

some_obj['greet']

Notez que la valeur d'index est une chaîne . Donc, dans votre directive, vous pouvez simplement faire ceci:

  link: function (scope, element, attrs) {

    element.bind('click', function (e) {

      if (confirm(attrs.confirm)) {
        var func_str = attrs.confirmAction;
        var func_name = func_str.substring(0, func_str.indexOf('(')); // Or func_str.match(/[^(]+/)[0];
        func_name = func_name.trim();

        console.log(func_name); // => doIt
        scope[func_name]();
      }
    });
  }
7stud
la source
+1 car l'idée d'analyser la fonction était délicate et m'a aidé avec un problème similaire mais un autre. Merci!
Anmol Saraf