Llame a una función de controlador desde una directiva sin alcance aislado en AngularJS

95

Parece que no puedo encontrar una manera de llamar a una función en el ámbito principal desde una directiva sin utilizar un ámbito aislado. Sé que si uso un alcance aislado, puedo usar "&" en el aislado para acceder a la función en el alcance principal, pero usar un alcance aislado cuando no es necesario tiene consecuencias. Considere el siguiente HTML:

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

En este ejemplo simple, quiero mostrar un cuadro de diálogo de confirmación de JavaScript y solo llamar a doIt () si hacen clic en "Aceptar" en el cuadro de diálogo de confirmación. Esto es simple usando un alcance aislado. La directiva se vería así:

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

Pero el problema es que, debido a que estoy usando un alcance aislado, ng-hide en el ejemplo anterior ya no se ejecuta en el alcance principal , sino en el alcance aislado (ya que el uso de un alcance aislado en cualquier directiva hace que todas las directivas en ese elemento utilizar el alcance aislado). Aquí hay un jsFiddle del ejemplo anterior donde ng-hide no funciona. (Tenga en cuenta que en este violín, el botón debe ocultarse cuando escribe "sí" en el cuadro de entrada).

La alternativa sería NO utilizar un alcance aislado , que en realidad es lo que realmente quiero aquí, ya que no es necesario que el alcance de esta directiva esté aislado. El único problema que tengo es, ¿cómo llamo a un método en el alcance principal si no lo paso en un alcance aislado ?

Aquí hay un jsfiddle en el que NO estoy usando un alcance aislado y el ng-hide funciona bien, pero, por supuesto, la llamada a confirmAction () no funciona y no sé cómo hacer que funcione.

Tenga en cuenta que la respuesta que realmente estoy buscando es cómo llamar a funciones en el alcance externo SIN usar un alcance aislado. Y no estoy interesado en hacer que este diálogo de confirmación funcione de otra manera, porque el objetivo de esta pregunta es averiguar cómo realizar llamadas al alcance externo y aún poder hacer que otras directivas funcionen en contra del alcance principal.

Alternativamente, me interesaría conocer soluciones que utilicen un alcance aislado si otras directivas aún funcionan en contra del alcance principal , pero no creo que esto sea posible.

Jim Cooper
fuente
Para aquellos que pasan, Dan Wahlin ha escrito un muy buen artículo que explica el alcance aislado y los parámetros de función: weblogs.asp.net/dwahlin/…
tanguy_k

Respuestas:

117

Dado que la directiva solo llama a una función (y no intenta establecer un valor en una propiedad), puede usar $ eval en lugar de $ parse (con un alcance no aislado):

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

O mejor, simplemente use $ apply , que $ eval () evaluará su argumento contra el alcance:

scope.$apply(attrs.confirmAction);

Violín

Mark Rajcok
fuente
No lo sabía, gracias por eso. Siempre pensé que el método $ parse era demasiado prolijo.
Clark Pan
2
¿Cómo establecerías los parámetros de esa función?
CMCDragonkai
2
@CMCDragonkai, si la función tiene argumentos, puede especificarlos en el HTML confirm-action="doIt(arg1)", y luego establecerlos scope.arg1antes de llamar a $ eval. Sin embargo, probablemente sería más limpio usar $ parse: stackoverflow.com/a/16200618/215945
Mark Rajcok
¿No es posible hacer el alcance. $ Eval (attrs.doIt ({arg1: 'blah'}) ;?
CMCDragonkai
3
@CMCDragonkai, no, scope.$eval(attrs.confirmAction({arg1: 'blah'})no funcionará. Necesitaría usar $ parse con esa sintaxis.
Mark Rajcok
17

Desea hacer uso del servicio $ parse en AngularJS.

Entonces, para tu ejemplo:

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

Hay un problema con configurar una propiedad usando $ parse, pero te dejaré cruzar ese puente cuando llegues a él.

Clark Pan
fuente
Gracias, Clark, esto es exactamente lo que estaba buscando. Intenté usar $ parse, pero no pude pasar el alcance, por lo que no me funcionó. Esto es perfecto.
Jim Cooper
Me sorprendió descubrir que esta solución también funciona sin alcance: cierto. Mi entendimiento era ese alcance: verdadero es lo que hace que el alcance de la directiva herede prototípicamente del alcance principal. ¿Alguna idea de por qué esto todavía funciona sin él?
Jim Cooper
2
ningún alcance secundario (eliminación scope:true) funciona porque la directiva no creará un nuevo alcance en absoluto y, por lo tanto, la scopepropiedad a la que hace referencia en la linkfunción es exactamente el mismo alcance que el alcance 'principal' anteriormente.
Clark Pan
$ aplicar es suficiente para este caso (ver mi respuesta).
Mark Rajcok
1
¿Para qué es el evento $?
CMCDragonkai
12

Llame explícitamente hideButtonal ámbito principal.

Aquí está el violín: http://jsfiddle.net/pXej2/5/

Y aquí está el HTML actualizado:

<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
fuente
+1 para una solución de trabajo. De hecho, había intentado usar $ parent y cuando no funcionó me di por vencido. Después de ver su solución, miré más de cerca y resultó que no podía agregar $ parent a los parámetros que estaba pasando (no se muestra en mi violín original). Por ejemplo, si quisiera pasar showIt a la función hideButton (), necesitaría usar $ parent.hideButton ($ parent.showIt) y me faltaba el segundo $ parent. Sin embargo, voy a aceptar la respuesta de Clark, ya que esa solución no requiere que el usuario de mi directiva sepa que debe agregar $ parent. ¡Gracias por la información!
Jim Cooper
1

El único problema que tengo es, ¿cómo llamo a un método en el alcance principal si no lo paso en un alcance aislado?

Aquí hay un jsfiddle donde NO estoy usando un alcance aislado y ng-hide está funcionando bien, pero, por supuesto, la llamada a confirmAction () no funciona y no sé cómo hacer que funcione.

Primero, un pequeño punto: en este caso, el alcance del controlador externo no es el alcance principal del alcance de la directiva; el alcance del controlador exterior es alcance de la directiva. En otras palabras, los nombres de variables usados ​​en la directiva se buscarán directamente en el alcance del controlador.

A continuación, la escritura attrs.confirmAction()no funciona porque attrs.confirmActionpara este botón,

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

es la cadena "doIt()", y no puede llamar a una cadena, por ejemplo "doIt()"().

Tu pregunta realmente es:

¿Cómo llamo a una función cuando tengo su nombre como una cadena?

En JavaScript, la mayoría de las funciones se llaman mediante notación de puntos, como esta:

some_obj.greet()

Pero también puede usar la notación de matriz:

some_obj['greet']

Tenga en cuenta que el valor del índice es una cadena . Entonces, en su directiva, simplemente puede hacer esto:

  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
fuente
+1 ya que la idea de analizar la función era complicada y me ayudó con un problema similar pero con otro. ¡Gracias!
Anmol Saraf