Une directive angulaire peut-elle passer des arguments aux fonctions dans les expressions spécifiées dans les attributs de la directive?

160

J'ai une directive de formulaire qui utilise un callbackattribut spécifié avec une portée d'isolat:

scope: { callback: '&' }

Il se trouve à l'intérieur d'un ng-repeatafin que l'expression que je passe inclut le idde l'objet comme argument de la fonction de rappel:

<directive ng-repeat = "item in stuff" callback = "callback(item.id)"/>

Quand j'ai fini avec la directive, elle appelle $scope.callback()depuis sa fonction de contrôleur. Dans la plupart des cas, c'est bien, et c'est tout ce que je veux faire, mais parfois j'aimerais ajouter un autre argument de l'intérieur de directivelui - même.

Existe-t-il une expression angulaire qui permettrait ceci $scope.callback(arg2):, résultant en callbackun appel avec arguments = [item.id, arg2]?

Sinon, quelle est la meilleure façon de procéder?

J'ai trouvé que cela fonctionne:

<directive 
  ng-repeat = "item in stuff" 
  callback = "callback" 
  callback-arg="item.id"/>

Avec

scope { callback: '=', callbackArg: '=' }

et la directive appelant

$scope.callback.apply(null, [$scope.callbackArg].concat([arg2, arg3]) );

Mais je ne pense pas que ce soit particulièrement soigné et que cela implique de mettre des éléments supplémentaires dans la portée d'isolat.

Y a-t-il un meilleur moyen?

Plunker aire de jeux ici (avoir la console ouverte).

Ed Hinchliffe
la source
L'attribut nommant "callback =" induit en erreur. C'est vraiment une évaluation de rappel, pas un rappel lui-même.
Dmitri Zaitsev
@DmitriZaitsev est une expression angulaire de rappel qui s'évaluera en fonction JavaScript. Je pense qu'il est assez évident que ce n'est pas une fonction JavaScript en soi. C'est juste une préférence mais je préférerais ne pas avoir à suffixer tous mes attributs avec "-expression". Ceci est cohérent avec l' ngAPI, par exemple ng-click="someFunction()"une expression qui évalue l'exécution d'une fonction.
Ed Hinchliffe
Je n'ai jamais vu l'expression angulaire appelée "callback". C'est toujours une fonction que l'on passe pour être appelée, d'où le nom. Vous utilisez même une fonction appelée "callback" dans votre exemple, pour rendre les choses encore plus confuses.
Dmitri Zaitsev
Je ne sais pas si vous êtes confus ou moi. Dans mon exemple, il $scope.callbackest défini par l' callback="someFunction"attribut et la scope: { callback: '=' }propriété de l'objet de définition de directive. $scope.callback est une fonction à appeler ultérieurement. La valeur réelle de l'attribut est évidemment une chaîne - c'est toujours le cas avec HTML.
Ed Hinchliffe
Vous nommez à la fois l'attribut et la fonction de la même manière - "rappel". C'est la recette de la confusion. Facile à éviter vraiment.
Dmitri Zaitsev

Réponses:

215

Si vous déclarez votre rappel comme mentionné par @ lex82 comme

callback = "callback(item.id, arg2)"

Vous pouvez appeler la méthode de rappel dans la portée de la directive avec la mappe d'objets et elle ferait la liaison correctement. Comme

scope.callback({arg2:"some value"});

sans avoir besoin de $ parse. Voir mon violon (journal de la console) http://jsfiddle.net/k7czc/2/

Mise à jour : il y a un petit exemple de ceci dans la documentation :

& ou & attr - fournit un moyen d'exécuter une expression dans le contexte de la portée parent. Si aucun nom attr n'est spécifié, le nom d'attribut est supposé être le même que le nom local. Étant donné et la définition du widget de la portée: {localFn: '& myAttr'}, puis isoler la propriété de portée localFn pointera vers un wrapper de fonction pour l'expression count = count + value. Il est souvent souhaitable de transmettre des données de la portée isolée via une expression et à la portée parente, cela peut être fait en passant une carte des noms et des valeurs de variables locales dans le wrapper d'expression fn. Par exemple, si l'expression est incrément (montant), nous pouvons spécifier la valeur du montant en appelant le localFn en tant que localFn ({montant: 22}).

Chandermani
la source
4
Très agréable! Est-ce documenté quelque part?
ach
12
Je ne pense pas que ce soit une bonne solution car dans la définition directive, parfois vous ne savez pas quel est le paramètre à transmettre.
OMGPOP
C'est une bonne solution et je vous en remercie, mais je pense que la réponse nécessite un peu de remontée. Qui est lex82 et qu'a-t-il mentionné?
Wtower
Approche intéressante. Bien que se passe-t-il lorsque vous souhaitez autoriser la transmission d'une fonction avec un paramètre (ou multiple)? Vous ne savez rien de la fonction ni de ses paramètres et devez l'exécuter sur un événement à l'intérieur d'une directive. Comment s'y prendre? Par exemple, sur une directive, vous pourriez avoir onchangefunc = 'myCtrlFunc (dynamicVariableHere)'
trainoasis
58

Rien de mal avec les autres réponses, mais j'utilise la technique suivante pour passer des fonctions dans un attribut de directive.

Laissez les parenthèses lorsque vous incluez la directive dans votre html:

<my-directive callback="someFunction" />

Puis «dépliez» la fonction dans le lien ou le contrôleur de votre directive. Voici un exemple:

app.directive("myDirective", function() {

    return {
        restrict: "E",
        scope: {
            callback: "&"                              
        },
        template: "<div ng-click='callback(data)'></div>", // call function this way...
        link: function(scope, element, attrs) {
            // unwrap the function
            scope.callback = scope.callback(); 

            scope.data = "data from somewhere";

            element.bind("click",function() {
                scope.$apply(function() {
                    callback(data);                        // ...or this way
                });
            });
        }
    }
}]);    

L'étape de "dépliage" permet d'appeler la fonction en utilisant une syntaxe plus naturelle. Cela garantit également que la directive fonctionne correctement même lorsqu'elle est imbriquée dans d'autres directives susceptibles de transmettre la fonction. Si vous n'avez pas fait le déballage, alors si vous avez un scénario comme celui-ci:

<outer-directive callback="someFunction" >
    <middle-directive callback="callback" >
        <inner-directive callback="callback" />
    </middle-directive>
</outer-directive>

Ensuite, vous vous retrouveriez avec quelque chose comme ça dans votre directive interne:

callback()()()(data); 

Ce qui échouerait dans d'autres scénarios d'imbrication.

J'ai adapté cette technique à partir d'un excellent article de Dan Wahlin à http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters

J'ai ajouté l'étape de déballage pour rendre l'appel de la fonction plus naturel et pour résoudre le problème d'imbrication que j'avais rencontré dans un projet.

ItsCosmo
la source
2
Une belle approche mais je ne peux pas utiliser le thispointeur à l'intérieur de la méthode de rappel, car il utilise la portée de la directive. J'utilise Typescript et mon rappel ressemble à ceci:public validateFirstName(firstName: string, fieldName: string): ng.IPromise<boolean> { var deferred = this.mQService.defer<boolean>(); ... .then(() => deferred.resolve(true)) .catch((msg) => { deferred.reject(false); }); return deferred.promise; }
ndee
1
Remarque: si vous avez des directives imbriquées et que vous souhaitez propager le rappel vers le haut, vous devez déballer dans chaque directive, pas seulement celle déclenchant le rappel.
Episodex
44

Dans la directive ( myDirective):

...
directive.scope = {  
    boundFunction: '&',
    model: '=',
};
...
return directive;

Dans le modèle de directive:

<div 
data-ng-repeat="item in model"  
data-ng-click='boundFunction({param: item})'>
{{item.myValue}}
</div>

En source:

<my-directive 
model='myData' 
bound-function='myFunction(param)'>
</my-directive>

...où myFunction est défini dans le contrôleur.

Notez que paramdans le modèle de directive se lie parfaitement à paramdans la source et est défini sur item.


Pour appeler depuis la linkpropriété d'une directive ("à l'intérieur" de celle-ci), utilisez une approche très similaire:

...
directive.link = function(isolatedScope) {
    isolatedScope.boundFunction({param: "foo"});
};
...
return directive;
Ben
la source
En ayant In source: bound-function = 'myFunction (obj1.param, obj2.param)'> alors comment procéder?
Ankit Pandey le
15

Oui, il existe un meilleur moyen: vous pouvez utiliser le service $ parse dans votre directive pour évaluer une expression dans le contexte de la portée parente tout en liant certains identificateurs de l'expression à des valeurs visibles uniquement dans votre directive:

$parse(attributes.callback)(scope.$parent, { arg2: yourSecondArgument });

Ajoutez cette ligne à la fonction de lien de la directive où vous pouvez accéder aux attributs de la directive.

Votre attribut de rappel peut alors être défini comme callback = "callback(item.id, arg2)"parce que arg2 est lié à yourSecondArgument par le service $ parse à l'intérieur de la directive. Les directives telles ng-clickque vous permettent d'accéder à l'événement click via l' $eventidentifiant à l'intérieur de l'expression passée à la directive en utilisant exactement ce mécanisme.

Notez que vous ne devez pas faire callbackun membre de votre étendue isolée avec cette solution.

lex82
la source
3
Utiliser scope.$parentrend la directive "fuyante" - elle "connaît" trop le monde extérieur, ce qu'un composant encapsulé bien conçu ne devrait pas.
Dmitri Zaitsev
3
Eh bien, il sait qu'il a une portée parent, mais il n'accède pas à un champ particulier de la portée, donc je pense que c'est tolérable.
lex82
0

Pour moi, la suite a fonctionné:

dans la directive, déclarez-le comme ceci:

.directive('myDirective', function() {
    return {
        restrict: 'E',
        replace: true,
        scope: {
            myFunction: '=',
        },
        templateUrl: 'myDirective.html'
    };
})  

Dans le modèle de directive, utilisez-le de la manière suivante:

<select ng-change="myFunction(selectedAmount)">

Et puis lorsque vous utilisez la directive, passez la fonction comme ceci:

<data-my-directive
    data-my-function="setSelectedAmount">
</data-my-directive>

Vous passez la fonction par sa déclaration et elle est appelée à partir de la directive et les paramètres sont renseignés.

michal.jakubeczy
la source