Método de llamada en controlador de directiva desde otro controlador

118

Tengo una directiva que tiene su propio controlador. Vea el siguiente código:

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'
    }

});

Esto está destinado a ser un sistema de notificación de errores / notificaciones / advertencias. Lo que quiero hacer es desde otro controlador (no uno de directiva) para llamar a la función showen este controlador. Y cuando hago eso, también me gustaría que mi función de enlace detectara que algunas propiedades cambiaron y realizara algunas animaciones.

Aquí hay un código para ejemplificar lo que estoy pidiendo:

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

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

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

Por lo tanto, al llamar showal popdowncontrolador de la directiva, la función de enlace también debe activarse y realizar una animación. ¿Cómo podría lograr eso?

user253530
fuente
¿Dónde está colocando la llamada a la popdowndirectiva en la página? ¿Es solo en un lugar donde se supone que otros controladores tienen acceso a ella, o hay varias ventanas emergentes en diferentes lugares?
Satchmorun
mi index.html tiene esto: <div ng-view> </div> <div popdown> </div> básicamente solo hay 1 instancia emergente ya que está destinado a estar disponible globalmente.
user253530
1
Creo que pretendías escribir en popdown.show(...)lugar de, ¿no popdown.notify(...)es así? De lo contrario, la función de notificación es algo confusa.
lanoxx
¿de dónde viene desde popdown.notify? .notifiymétodo, quiero decir
Verde

Respuestas:

167

Esta es una pregunta interesante, y comencé a pensar en cómo implementaría algo como esto.

Se me ocurrió este (violín) ;

Básicamente, en lugar de intentar llamar a una directiva desde un controlador, creé un módulo para albergar toda la lógica emergente:

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

Puse dos cosas en el módulo, una factorypara la API que se puede inyectar en cualquier lugar y la otra directivepara definir el comportamiento del elemento emergente real:

El habitante define un par de funciones successy errory realiza un seguimiento de un par 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 directiva hace que la API se inyecte en su controlador y observa la API en busca de cambios (estoy usando bootstrap css por conveniencia):

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>'
    }
})

Luego defino un appmódulo que depende 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); }
});

Y el HTML se ve así:

<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>

No estoy seguro de si es completamente ideal, pero parecía una forma razonable de configurar la comunicación con una directiva emergente global.

Nuevamente, como referencia, el violín .

satchmorun
fuente
10
+1 Nunca se debe llamar a una función en una directiva desde fuera de la directiva; es una mala práctica. Usar un servicio para administrar el estado global que lee una directiva es muy común y este es el enfoque correcto. Más aplicaciones incluyen colas de notificación y cuadros de diálogo modales.
Josh David Miller
7
¡Respuesta realmente excepcional! Un ejemplo tan útil para aquellos de nosotros que venimos de jQuery y Backbone
Brandon
11
De esta forma, ¿es posible utilizar este módulo para crear instancias de varias directivas en la misma vista? ¿Cómo puedo llamar a la función de éxito o error de una instancia particular de esta directiva?
ira
3
@ira, probablemente podría cambiar la fábrica para mantener un mapa (o lista) de objetos de estado y mensaje y luego usar un atributo de nombre en la directiva para identificar qué elemento de la lista necesita. Entonces, en lugar de llamar success(msg)al html, llamaría sucess(name, msg)para seleccionar la directiva con el nombre correcto.
lanoxx
5
@JoshDavidMiller ¿por qué consideras que es una mala práctica llamar a un método en una directiva? Si una directiva encapsula alguna lógica DOM como se pretendía, seguramente es bastante natural exponer una API para que los controladores que la usan puedan invocar sus métodos según sea necesario.
Paul Taylor
27

También puede utilizar eventos para activar el Popdown.

Aquí hay un violín basado en la solución de satchmorun. Prescinde de PopdownAPI, y el controlador de nivel superior en lugar de $broadcastlos eventos de 'éxito' y 'error' en la cadena de alcance:

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

El módulo Popdown luego registra las funciones del controlador para estos eventos, por ejemplo:

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

Esto funciona, al menos, y me parece una solución bien desacoplada. Dejaré que otros intervengan si esto se considera una mala práctica por alguna razón.

Aron
fuente
1
Un inconveniente en el que puedo pensar es que en la respuesta seleccionada solo necesita PopdownAPI (fácilmente disponible con DI). En este, necesita acceso al alcance del controlador para transmitir el mensaje. De todos modos, parece muy conciso.
Julián
Me gusta más esto que el enfoque de servicio para casos de uso simples, ya que mantiene baja la complejidad y aún está poco acoplado
Patrick Favre
11

También puede exponer el controlador de la directiva al ámbito principal, como hace ngFormcon el nameatributo: http://docs.angularjs.org/api/ng.directive:ngForm

Aquí puede encontrar un ejemplo muy básico de cómo se podría lograr http://plnkr.co/edit/Ps8OXrfpnePFvvdFgYJf?p=preview

En este ejemplo, tengo un myDirectivecontrolador dedicado con $clearmétodo (una especie de API pública muy simple para la directiva). Puedo publicar este controlador en el ámbito principal y usar llamar a este método fuera de la directiva.

luacassus
fuente
Esto requiere una relación entre los controladores, ¿verdad? Dado que OP quería un centro de mensajes, esto puede no ser ideal para él. Pero también fue muy agradable aprender su enfoque. Es útil en muchas situaciones y, como dijiste, el propio angular lo usa.
fasfsfgs
Estoy tratando de seguir un ejemplo proporcionado por satchmorun. Estoy generando algo de html en el tiempo de ejecución, pero no estoy usando la plantilla de la directiva. Estoy usando el controlador de la directiva para especificar una función para llamar desde el html agregado, pero no se llama a la función. Básicamente, tengo esta directiva: directives.directive ('abcXyz', function ($ compile {return {restrict: 'AE', require: 'ngModel', controller: function ($ scope) {$ scope.function1 = function () {..};}, mi html es: "<a href="" ng-click="function1('itemtype')">
Mark
¡Esta es la única solución elegante que puede exponer la API de directiva si la directiva no es un singleton! Todavía no me gusta usarlo $scope.$parent[alias]porque me huele a usar una api angular interna. Pero todavía no puedo encontrar una solución más elegante para las directivas no singleton. Otras variantes como la transmisión de eventos o definir un objeto vacío en el controlador principal para la directiva api huele aún más.
Ruslan Stelmachenko
3

Tengo una solución mucho mejor.

aquí está mi directiva, he inyectado una referencia de objeto en la directiva y la he extendido agregando la función de invocación en el código de la directiva.

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
                    }
                }    
            });
        }
    };
});

Declarando la directiva en el HTML con un parámetro:

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

mi controlador:

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
fuente
Esto básicamente va en contra de los principios de separación de preocupaciones. Usted proporciona a la directiva un objeto instanciado en un controlador, y delega la responsabilidad de administrar ese objeto (es decir, la creación de la función de invocación) a la directiva. En mi opinión, NO es la mejor solución.
Florin Vistig