Chame uma função de controlador de uma diretiva sem escopo isolado em AngularJS

95

Não consigo encontrar uma maneira de chamar uma função no escopo pai de uma diretiva sem usar o escopo isolado. Eu sei que se eu usar o escopo isolado, posso apenas usar "&" no isolado para acessar a função no escopo pai, mas usar o escopo isolado quando não é necessário tem consequências. Considere o seguinte HTML:

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

Neste exemplo simples, quero mostrar uma caixa de diálogo de confirmação de JavaScript e apenas chamar doIt () se clicar em "OK" na caixa de diálogo de confirmação. Isso é simples usando um escopo isolado. A diretiva ficaria assim:

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

Mas o problema é que, como estou usando escopo isolado, ng-hide no exemplo acima não é mais executado no escopo pai , mas sim no escopo isolado (uma vez que usar um escopo isolado em qualquer diretiva faz com que todas as diretivas nesse elemento usar o escopo isolado). Aqui está um jsFiddle do exemplo acima em que ng-hide não está funcionando. (Observe que neste violino, o botão deve se ocultar quando você digita "sim" na caixa de entrada.)

A alternativa seria NÃO utilizar um âmbito de aplicação isolado , o que de facto é o que pretendo aqui, uma vez que não há necessidade de isolar o âmbito desta directiva. O único problema que tenho é como chamo um método no escopo pai se não o transmito no escopo isolado ?

Aqui está um jsfiddle onde NÃO estou usando escopo isolado e o ng-hide está funcionando bem, mas, é claro, a chamada para confirmAction () não funciona e não sei como fazê-la funcionar.

Observe que a resposta que realmente estou procurando é como chamar funções no escopo externo SEM usar um escopo isolado. E não estou interessado em fazer com que esse diálogo de confirmação funcione de outra maneira, porque o objetivo desta questão é descobrir como fazer chamadas para o escopo externo e ainda ser capaz de fazer outras diretivas funcionarem contra o escopo pai.

Como alternativa, eu estaria interessado em ouvir sobre soluções que usam um escopo isolado se outras diretivas ainda funcionarem contra o escopo pai , mas não acho que isso seja possível.

Jim Cooper
fonte
Para aqueles que estão de passagem, Dan Wahlin escreveu um artigo muito bom explicando o escopo isolado e os parâmetros de função: weblogs.asp.net/dwahlin/…
tanguy_k

Respostas:

117

Como a diretiva está apenas chamando uma função (e não tentando definir um valor em uma propriedade), você pode usar $ eval em vez de $ parse (com um escopo não isolado):

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

Ou melhor, simplesmente use $ apply , que $ eval () uate seu argumento contra o escopo:

scope.$apply(attrs.confirmAction);

Violino

Mark Rajcok
fonte
Não sabia disso, obrigado por isso. Sempre achei o método $ parse um pouco prolixo.
Clark Pan
2
como você definiria os parâmetros dessa função?
CMCDragonkai
2
@CMCDragonkai, se a função tiver argumentos, você pode especificá-los no HTML confirm-action="doIt(arg1)", e, em seguida, definir scope.arg1antes de chamar $ eval. No entanto, provavelmente seria mais limpo usar $ parse: stackoverflow.com/a/16200618/215945
Mark Rajcok
Não é possível fazer o escopo. $ Eval (attrs.doIt ({arg1: 'blah'}) ;?
CMCDragonkai
3
@CMCDragonkai, não, scope.$eval(attrs.confirmAction({arg1: 'blah'})não funcionará. Você precisaria usar $ parse com essa sintaxe.
Mark Rajcok
17

Você deseja usar o serviço $ parse no AngularJS.

Então, para seu exemplo:

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

Existe uma pegadinha a ver com definir uma propriedade usando $ parse, mas vou deixar você cruzar essa ponte quando chegar a ela.

Clark Pan
fonte
Obrigado, Clark, é exatamente isso que eu estava procurando. Eu tentei usar $ parse, mas não consegui passar o escopo, então não estava funcionando para mim. Isto é perfeito.
Jim Cooper
Fiquei surpreso ao descobrir que essa solução também funciona sem escopo: verdadeiro. Meu entendimento era esse escopo: verdade é o que faz o escopo da diretiva herdar prototipicamente do escopo pai. Alguma ideia de por que isso ainda funciona sem ele?
Jim Cooper
2
nenhum escopo filho (exclusão scope:true) funciona porque a diretiva não criará um novo escopo e, portanto, a scopepropriedade que você faz referência na linkfunção tem exatamente o mesmo escopo que o escopo 'pai' anterior.
Clark Pan
$ apply é suficiente para este caso (veja minha resposta).
Mark Rajcok
1
Para que serve o $ event?
CMCDragonkai
12

Chame explicitamente hideButtono escopo pai.

Aqui está o violino: http://jsfiddle.net/pXej2/5/

E aqui está o HTML atualizado:

<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
fonte
1 para uma solução de trabalho. Na verdade, tentei usar $ parent e, quando não funcionou, desisti. Depois de ver sua solução, observei mais de perto e descobri que não estava adicionando $ parent aos parâmetros que estava transmitindo (não mostrado em meu violino original). Por exemplo, se eu quisesse passar showIt para a função hideButton (), eu precisaria usar $ parent.hideButton ($ parent.showIt) e estava faltando o segundo $ parent. Vou aceitar a resposta de Clark, entretanto, uma vez que essa solução não exige que o usuário de minha diretiva saiba que precisa adicionar $ parent. Obrigado pelo insight!
Jim Cooper
1

O único problema que tenho é como chamo um método no escopo pai se não o transmito no escopo isolado?

Aqui está um jsfiddle onde NÃO estou usando escopo isolado e o ng-hide está funcionando bem, mas, é claro, a chamada para confirmAction () não funciona e não sei como fazê-la funcionar.

Em primeiro lugar, um pequeno ponto: neste caso, o âmbito do controlador externo não é o âmbito principal do âmbito da diretiva; âmbito do controlador externo é o âmbito da directiva. Em outras palavras, os nomes das variáveis ​​usados ​​na diretiva serão pesquisados ​​diretamente no escopo do controlador.

Em seguida, escrever attrs.confirmAction()não funciona porque attrs.confirmActionpara este botão,

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

é a string "doIt()"e você não pode chamar uma string, por exemplo "doIt()"().

Sua pergunta realmente é:

Como chamo uma função quando tenho seu nome como uma string?

Em JavaScript, você geralmente chama funções usando notação de ponto, como este:

some_obj.greet()

Mas você também pode usar a notação de matriz:

some_obj['greet']

Observe como o valor do índice é uma string . Portanto, em sua diretiva, você pode simplesmente fazer isso:

  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
fonte
1 uma vez que a ideia de analisar a função era complicada e me ajudou com um problema semelhante, mas outro. Obrigado!
Anmol Saraf