Méthode d'appel dans le contrôleur de directive à partir d'un autre contrôleur

118

J'ai une directive qui a son propre contrôleur. Voir le code ci-dessous:

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

popdown.directive('popdown', function () {
    var PopdownController = function ($scope) {
        this.scope = $scope;
    }

    PopdownController.prototype = {
        show:function (message, type) {
            this.scope.message = message;
            this.scope.type = type;
        },

        hide:function () {
            this.scope.message = '';
            this.scope.type = '';
        }
    }

    var linkFn = function (scope, lElement, attrs, controller) {

    };

    return {
        controller: PopdownController,
        link: linkFn,
        replace: true,
        templateUrl: './partials/modules/popdown.html'
    }

});

Il s'agit d'un système de notification des erreurs / notifications / avertissements. Ce que je veux faire, c'est à partir d'un autre contrôleur (pas de directive) pour appeler la fonction showsur ce contrôleur. Et quand je fais cela, je voudrais également que ma fonction de lien détecte que certaines propriétés ont changé et exécute certaines animations.

Voici un code pour illustrer ce que je demande:

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

app.controller('IndexController', function($scope, RestService) {
    var result = RestService.query();

    if(result.error) {
        popdown.notify(error.message, 'error');
    }
});

Ainsi, lors de l'appel showau popdowncontrôleur de directive, la fonction de liaison doit également être déclenchée et effectuer une animation. Comment pourrais-je y parvenir?

user253530
la source
Où placez-vous l'appel à la popdowndirective sur la page - est-ce juste à un endroit où les autres contrôleurs sont censés tous y avoir accès, ou y a-t-il plusieurs fenêtres contextuelles à différents endroits?
satchmorun
mon index.html a ceci: <div ng-view> </div> <div popdown> </div> fondamentalement, il n'y a qu'une seule instance popdown car elle est censée être disponible globalement.
user253530
1
Je pense que tu voulais écrire popdown.show(...)au lieu de popdown.notify(...)ça, n'est-ce pas? Sinon, la fonction de notification est un peu déroutante.
lanoxx
d'où vient-il popdown.notify? .notifiyméthode, je veux dire
Vert

Réponses:

167

C'est une question intéressante, et j'ai commencé à réfléchir à la manière dont je mettrais en œuvre quelque chose comme ça.

Je suis venu avec ceci (violon) ;

Fondamentalement, au lieu d'essayer d'appeler une directive à partir d'un contrôleur, j'ai créé un module pour héberger toute la logique de popdown:

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

J'ai mis deux choses dans le module, une factorypour l'API qui peut être injectée n'importe où, et la directivepour définir le comportement de l'élément popdown réel:

L'usine définit seulement quelques fonctions successet erroret assure le suivi d'un couple de variables:

PopdownModule.factory('PopdownAPI', function() {
    return {
        status: null,
        message: null,
        success: function(msg) {
            this.status = 'success';
            this.message = msg;
        },
        error: function(msg) {
            this.status = 'error';
            this.message = msg;
        },
        clear: function() {
            this.status = null;
            this.message = null;
        }
    }
});

La directive obtient l'API injectée dans son contrôleur et surveille les modifications de l'API (j'utilise bootstrap css pour plus de commodité):

PopdownModule.directive('popdown', function() {
    return {
        restrict: 'E',
        scope: {},
        replace: true,
        controller: function($scope, PopdownAPI) {
            $scope.show = false;
            $scope.api = PopdownAPI;

            $scope.$watch('api.status', toggledisplay)
            $scope.$watch('api.message', toggledisplay)

            $scope.hide = function() {
                $scope.show = false;
                $scope.api.clear();
            };

            function toggledisplay() {
                $scope.show = !!($scope.api.status && $scope.api.message);               
            }
        },
        template: '<div class="alert alert-{{api.status}}" ng-show="show">' +
                  '  <button type="button" class="close" ng-click="hide()">&times;</button>' +
                  '  {{api.message}}' +
                  '</div>'
    }
})

Ensuite, je définis un appmodule qui dépend de Popdown:

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

app.controller('main', function($scope, PopdownAPI) {
    $scope.success = function(msg) { PopdownAPI.success(msg); }
    $scope.error   = function(msg) { PopdownAPI.error(msg); }
});

Et le HTML ressemble à:

<html ng-app="app">
    <body ng-controller="main">
        <popdown></popdown>
        <a class="btn" ng-click="success('I am a success!')">Succeed</a>
        <a class="btn" ng-click="error('Alas, I am a failure!')">Fail</a>
    </body>
</html>

Je ne suis pas sûr que ce soit complètement idéal, mais cela semblait être un moyen raisonnable de mettre en place une communication avec une directive popdown globale.

Encore une fois, pour référence, le violon .

sacoche
la source
10
+1 Il ne faut jamais appeler une fonction dans une directive en dehors de la directive - c'est une mauvaise pratique. L'utilisation d'un service pour gérer l'état global qu'une directive lit est très courante et c'est la bonne approche. D'autres applications incluent des files d'attente de notification et des boîtes de dialogue modales.
Josh David Miller le
7
Réponse vraiment exceptionnelle! Un exemple si utile pour ceux d'entre nous venant de jQuery et Backbone
Brandon
11
De cette manière, est-il possible d'utiliser ce module pour instancier plusieurs directives dans la même vue? Comment puis-je appeler la fonction de succès ou d'erreur d'une instance particulière de cette directive?
ira
3
@ira vous pouvez probablement changer la fabrique pour conserver une carte (ou une liste) des objets d'état et de message, puis utiliser un attribut de nom sur la directive pour identifier l'élément de la liste dont vous avez besoin. Donc, au lieu d'appeler success(msg)le code html, vous appelleriez sucess(name, msg)pour sélectionner la directive avec le nom correct.
lanoxx
5
@JoshDavidMiller pourquoi considérez-vous comme une mauvaise pratique d'appeler une méthode sur une directive? Si une directive encapsule la logique DOM comme prévu, il est sûrement assez naturel d'exposer une API afin que les contrôleurs qui l'utilisent puissent invoquer ses méthodes si nécessaire?
Paul Taylor
27

Vous pouvez également utiliser des événements pour déclencher le Popdown.

Voici un violon basé sur la solution de Satchmorun. Il se dispense de PopdownAPI, et du contrôleur de niveau supérieur à la place, $broadcastles événements de `` succès '' et `` erreur '' le long de la chaîne de portée:

$scope.success = function(msg) { $scope.$broadcast('success', msg); };
$scope.error   = function(msg) { $scope.$broadcast('error', msg); };

Le module Popdown enregistre ensuite les fonctions du gestionnaire pour ces événements, par exemple:

$scope.$on('success', function(event, msg) {
    $scope.status = 'success';
    $scope.message = msg;
    $scope.toggleDisplay();
});

Cela fonctionne, au moins, et me semble être une solution bien découplée. Je laisserai les autres intervenir si cela est considéré comme une mauvaise pratique pour une raison quelconque.

Aron
la source
1
Un inconvénient auquel je peux penser est que dans la réponse sélectionnée, vous n'avez besoin que du PopdownAPI (facilement disponible avec DI). Dans celui-ci, vous devez accéder à la portée du contrôleur pour diffuser le message. Quoi qu'il en soit, cela a l'air très concis.
Julian
J'aime mieux cela que l'approche de service pour les cas d'utilisation simples car elle réduit la complexité et est toujours faiblement couplée
Patrick Favre
11

Vous pouvez également exposer le contrôleur de la directive à la portée parent, comme le fait ngFormavec l' nameattribut: http://docs.angularjs.org/api/ng.directive:ngForm

Vous pouvez trouver ici un exemple très basique de la manière dont cela pourrait être réalisé http://plnkr.co/edit/Ps8OXrfpnePFvvdFgYJf?p=preview

Dans cet exemple, j'ai myDirectiveavec un contrôleur dédié avec une $clearméthode (sorte d'API publique très simple pour la directive). Je peux publier ce contrôleur dans la portée parent et utiliser appeler cette méthode en dehors de la directive.

Luacassus
la source
Cela nécessite une relation entre les contrôleurs, non? Puisque OP voulait un centre de messages, ce n'est peut-être pas l'idéal pour lui. Mais c'était aussi très agréable d'apprendre votre approche. C'est utile dans de nombreuses situations et, comme vous l'avez dit, angular lui-même l'utilise.
fasfsfgs
J'ai essayé de suivre un exemple fourni par satchmorun. Je génère du HTML au moment de l'exécution, mais je n'utilise pas le modèle de directive. J'utilise le contrôleur de directive pour spécifier une fonction à appeler à partir du code HTML ajouté, mais la fonction n'est pas appelée. Fondamentalement, j'ai cette directive: directives.directive ('abcXyz', function ($ compile {return {restrict: 'AE', require: 'ngModel', controller: function ($ scope) {$ scope.function1 = function () {..};}, mon code HTML est: "<a href="" ng-click="function1('itemtype')">
Marquer
C'est la seule solution élégante qui peut exposer une API de directive si la directive n'est pas un singleton! Je n'aime toujours pas utiliser $scope.$parent[alias]parce que ça sent pour moi d'utiliser une API angulaire interne. Mais je ne trouve toujours pas de solution plus élégante pour les directives not-singleton. D'autres variantes comme la diffusion d'événements ou la définition d'un objet vide dans le contrôleur parent pour une API directive sent encore plus.
Ruslan Stelmachenko
3

J'ai une bien meilleure solution.

voici ma directive, j'ai injecté sur la référence d'objet dans la directive et je l'ai étendue en ajoutant la fonction invoke dans le code directive.

app.directive('myDirective', function () {
    return {
        restrict: 'E',
        scope: {
        /*The object that passed from the cntroller*/
        objectToInject: '=',
        },
        templateUrl: 'templates/myTemplate.html',

        link: function ($scope, element, attrs) {
            /*This method will be called whet the 'objectToInject' value is changes*/
            $scope.$watch('objectToInject', function (value) {
                /*Checking if the given value is not undefined*/
                if(value){
                $scope.Obj = value;
                    /*Injecting the Method*/
                    $scope.Obj.invoke = function(){
                        //Do something
                    }
                }    
            });
        }
    };
});

Déclarer la directive dans le HTML avec un paramètre:

<my-directive object-to-inject="injectedObject"></ my-directive>

mon contrôleur:

app.controller("myController", ['$scope', function ($scope) {
   // object must be empty initialize,so it can be appended
    $scope.injectedObject = {};

    // now i can directly calling invoke function from here 
     $scope.injectedObject.invoke();
}];
Ashwini Jindal
la source
Cela va fondamentalement à l'encontre des principes de séparation des préoccupations. Vous fournissez à la directive un objet instancié dans un contrôleur, et vous déléguez la responsabilité de gérer cet objet (c'est-à-dire la création de la fonction d'appel) à la directive. À mon avis, PAS la meilleure solution.
Florin Vistig