Chamar AngularJS a partir do código legado

180

Estou usando o AngularJS para criar controles HTML que interagem com um aplicativo Flex herdado. Todos os retornos de chamada do aplicativo Flex devem ser anexados à janela DOM.

Por exemplo (no AS3)

ExternalInterface.call("save", data);

Chamará

window.save = function(data){
    // want to update a service 
    // or dispatch an event here...
}

De dentro da função de redimensionamento JS, eu gostaria de despachar um evento que um controlador possa ouvir. Parece que a criação de um serviço é o caminho a percorrer. Você pode atualizar um serviço de fora do AngularJS? Um controlador pode escutar eventos de um serviço? Em um experimento (clique para violino) , consegui acessar um serviço, mas a atualização dos dados do serviço não é refletida na exibição (no exemplo, um <option>deve ser adicionado ao <select>).

Obrigado!

grego
fonte
1
Observe que no jsfiddle acima o injetor é obtido sem direcionar um elemento no aplicativo usando var injector = angular.injector(['ng', 'MyApp']);. Fazer isso fornecerá um contexto completamente novo e uma duplicata myService. Isso significa que você terminará com duas instâncias do serviço e modelo e adicionará dados ao lugar errado. Em vez disso, você deve segmentar um elemento no aplicativo usando angular.element('#ng-app').injector(['ng', 'MyApp']). Nesse ponto, você pode usar $ apply para quebrar as alterações do modelo.
Thanh Nguyen

Respostas:

293

A interoperabilidade de fora de angular para angular é o mesmo que depurar aplicativos angulares ou integrar-se à biblioteca de terceiros.

Para qualquer elemento DOM, você pode fazer isso:

  • angular.element(domElement).scope() para obter o escopo atual do elemento
  • angular.element(domElement).injector() para obter o injetor de aplicativo atual
  • angular.element(domElement).controller()para se apossar da ng-controllerinstância.

No injetor, você pode obter qualquer serviço em aplicação angular. Da mesma forma, no escopo, você pode invocar qualquer método que tenha sido publicado nele.

Lembre-se de que quaisquer alterações no modelo angular ou invocações de métodos no escopo precisam ser agrupadas da $apply()seguinte maneira:

$scope.$apply(function(){
  // perform any model changes or method invocations here on angular app.
});
Misko Hevery
fonte
1
isso funciona, mas eu gostaria que houvesse alguma maneira de passar de um módulo diretamente para seu escopo - isso é possível? Tendo que voltar atrás e selecionar, o [ng-app]nó raiz parece retroceder quando eu já tenho uma referência ao módulo ...
mindplay.dk
5
Não consigo fazer isso funcionar: estou ligando angular.element(document.getElementById(divName)).scope(), mas não consigo invocar nenhuma função, apenas retorna "indefinido" no console.
Emil
1
Mesmo enfrentando o mesmo problema descrito acima por @Emil, ele está retornando indefinido. Qualquer ajuda ?
Bibin
5
O elemento (). scope () não funcionará se os dados de depuração estiverem desativados, que é a recomendação para produção. Isso não o torna inútil nesse cenário? Isso será apenas para teste / depuração.
K.Kitbert
4
Você não vai querer fazer isso no Angular 1.3. A equipe Angular não pretendia que chamassemos ".scope ()" nos elementos do código de produção. Era para ser uma ferramenta de depuração. Portanto, começando no Angular 1.3, você pode desativar isso. O Angular deixará de anexar o escopo ao elemento usando a função .data do jQuery. Isso irá acelerar seu aplicativo. Além disso, entregar seus escopos para os recursos de cache do jquery criará vazamentos de memória. Portanto, você definitivamente deve desativar isso, para acelerar seu aplicativo. O site da Angular possui um guia de produção que você deve usar para saber mais.
gelado
86

Misko deu a resposta correta (obviamente), mas alguns de nós, novatos, talvez precisemos mais dela.

Quando se trata de chamar o código AngularJS a partir de aplicativos herdados, pense no código AngularJS como um "micro aplicativo" existente em um contêiner protegido em seu aplicativo herdado. Você não pode fazer chamadas diretamente (por uma boa razão), mas pode fazer chamadas remotas por meio do objeto $ scope.

Para usar o objeto $ scope, você precisa obter o identificador de $ scope. Felizmente, isso é muito fácil de fazer.

Você pode usar a identificação de qualquer elemento HTML no HTML do seu "micro-aplicativo" AngularJS para obter o controle do escopo $ do aplicativo AngularJS.

Como exemplo, digamos que queremos chamar algumas funções em nosso controlador AngularJS, como sayHi () e sayBye (). No HTML do AngularJS (exibição), temos uma div com o ID "MySuperAwesomeApp". Você pode usar o seguinte código, combinado com o jQuery, para obter o identificador do $ scope:

var microappscope = angular.element($("#MySuperAwesomeApp")).scope();

Agora você pode chamar suas funções de código AngularJS por meio do identificador de escopo:

// we are in legacy code land here...

microappscope.sayHi();

microappscope.sayBye();

Para tornar as coisas mais convenientes, você pode usar uma função para agarrar o manipulador de escopo a qualquer momento que desejar acessá-lo:

function microappscope(){

    return angular.element($("#MySuperAwesomeApp")).scope();

}

Suas chamadas seriam assim:

microappscope().sayHi();

microappscope().sayBye();

Você pode ver um exemplo de trabalho aqui:

http://jsfiddle.net/peterdrinnan/2nPnB/16/

Também mostrei isso em uma apresentação de slides para o grupo Ottawa AngularJS (apenas pule para os últimos 2 slides)

http://www.slideshare.net/peterdrinnan/angular-for-legacyapps

Peter Drinnan
fonte
4
Observe que as respostas somente para links são desencorajadas; as respostas SO devem ser o ponto final de uma pesquisa por uma solução (em comparação com outra parada de referências, que tendem a ficar obsoletas ao longo do tempo). Considere adicionar aqui uma sinopse independente, mantendo o link como referência.
21413 kleopatra
Esclarecimentos adicionais agradáveis. Obrigado.
21413 Joe
1
Linda explicação! permitiu-me contornar uma validação de formulário, fazendo o seguinte:<input type="button" onclick="angular.element(this).scope().edit.delete();" value="delete">
Purefan
24

A melhor explicação do conceito que encontrei está aqui: https://groups.google.com/forum/#!msg/angular/kqFrwiysgpA/eB9mNbQzcHwJ

Para economizar o clique:

// get Angular scope from the known DOM element
e = document.getElementById('myAngularApp');
scope = angular.element(e).scope();
// update the model with a wrap in $apply(fn) which will refresh the view for us
scope.$apply(function() {
    scope.controllerMethod(val);
}); 
Homem sábio
fonte
14
O procedimento acima funciona quando o aplicativo e o controlador coexistem no mesmo elemento. Para aplicativos mais complexos que utilizam uma diretiva ng-view em um modelo, você deve obter o primeiro elemento dentro da visualização, não o elemento DOM do aplicativo inteiro. Eu tive que vasculhar elementos com um document.getElementsByClassName ('ng-scope'); lista de nós para descobrir o elemento DOM do escopo correto a ser capturado.
precisa saber é o seguinte
Sei que esse é um tópico muito antigo, mas acho que estou enfrentando esse problema. Alguém tem algum código que mostra como percorrer a lista para descobrir o elemento DOM para pegar?
JerryKur
Ignore minha pergunta. Consegui obter esse trabalho usando apenas document.getElementById ('qualquer controle que tenha uma diretiva NG'). Scope ().
JerryKur
se você utiliza o ng-view e divide seus pontos de vista em seus próprios arquivos. você pode colocar e idno elemento HTML superior, em seguida, fazer document.getElementById()esse ID. Isso fornece acesso ao escopo desse controlador. métodos / propriedades etc ... apenas colocando um ponto no comentário de goosemanjack.
Ftravers
13

Além das outras respostas. Se você não deseja acessar um método em um controlador, mas deseja acessar o serviço diretamente, pode fazer algo assim:

// Angular code* :
var myService = function(){
    this.my_number = 9;
}
angular.module('myApp').service('myService', myService);


// External Legacy Code:
var external_access_to_my_service = angular.element('body').injector().get('myService');
var my_number = external_access_to_my_service.my_number 
Alec Hewitt
fonte
13

Graças à postagem anterior, posso atualizar meu modelo com um evento assíncrono.

<div id="control-panel" ng-controller="Filters">
    <ul>
        <li ng-repeat="filter in filters">
        <button type="submit" value="" class="filter_btn">{{filter.name}}</button>
        </li>
    </ul>
</div>

Eu declaro meu modelo

function Filters($scope) {
    $scope.filters = [];
}

E eu atualizo meu modelo de fora do meu escopo

ws.onmessage = function (evt) {
    dictt = JSON.parse(evt.data);
    angular.element(document.getElementById('control-panel')).scope().$apply(function(scope){
        scope.filters = dictt.filters;
    });
};
Guillaume Vincent
fonte
6

Uma maneira mais segura e de desempenho, especialmente quando os dados de depuração estão desativados, é usar uma variável compartilhada para manter uma função de retorno de chamada. Seu controlador angular implementa esta função para retornar seus internos ao código externo.

var sharedVar = {}
myModule.constant('mySharedVar', sharedVar)
mymodule.controller('MyCtrl', [ '$scope','mySharedVar', function( $scope, mySharedVar) {

var scopeToReturn = $scope;

$scope.$on('$destroy', function() {
        scopeToReturn = null;
    });

mySharedVar.accessScope = function() {
    return scopeToReturn;
}
}]);

Generalizada como uma diretiva reutilizável:

Criei uma diretiva 'exposeScope' que funciona de maneira semelhante, mas o uso é mais simples:

<div ng-controller="myController" expose-scope="aVariableNameForThisScope">
   <span expose-scope='anotherVariableNameForTheSameScope" />
</div>

Isso armazena o escopo atual (que é fornecido à função de link da diretiva) em um objeto global de 'escopos' que é detentor de todos os escopos. O valor fornecido ao atributo de diretiva é usado como o nome da propriedade do escopo neste objeto global.

Veja a demonstração aqui . Como mostrei na demonstração, é possível disparar eventos jQuery quando o escopo é armazenado e removido do objeto global 'escopos'.

<script type="text/javascript" >
    $('div').on('scopeLinked', function(e, scopeName, scope, allScopes) {
      // access the scope variable or the given name or the global scopes object
    }.on('scopeDestroyed', function(e, scopeName, scope, allScopes) {
      // access the scope variable or the given name or the global scopes object
    }

</script>

Observe que, eu não testei o on ('scopeDestroyed') quando o elemento real é removido do DOM. Se não funcionar, acionar o evento no próprio documento em vez do elemento pode ajudar. (veja o app.js) no plunker de demonstração.

Cagatay Kalan
fonte
3

Sei que essa é uma pergunta antiga, mas eu estava procurando opções para fazer isso recentemente, então pensei em colocar minhas descobertas aqui, caso sejam úteis para alguém.

Na maioria dos casos, se houver a necessidade de código legado externo para interagir com o estado da interface do usuário ou com o funcionamento interno do aplicativo, um serviço pode ser útil para abstrair essas alterações. Se um código externo estiver interagindo diretamente com seu controlador angular, componente ou diretiva, você estará acoplando fortemente seu aplicativo ao código legado, o que é uma má notícia.

O que acabei usando no meu caso é uma combinação de globais acessíveis pelo navegador (janela) e manipulação de eventos. Meu código tem um mecanismo de geração de formulários inteligente que requer saída JSON de um CMS para iniciar os formulários. Aqui está o que eu fiz:

function FormSchemaService(DOM) {
    var conf = DOM.conf;

    // This event is the point of integration from Legacy Code 
    DOM.addEventListener('register-schema', function (e) {

       registerSchema(DOM.conf); 
    }, false);

    // service logic continues ....

O Serviço de Esquema de Formulário é criado usando o injetor angular conforme o esperado:

angular.module('myApp.services').
service('FormSchemaService', ['$window' , FormSchemaService ])

E nos meus controladores: function () {'use strict';

angular.module('myApp').controller('MyController', MyController);

MyEncapsulatorController.$inject = ['$scope', 'FormSchemaService'];

function MyController($scope, formSchemaService) {
    // using the already configured formSchemaService
    formSchemaService.buildForm(); 

Até agora, isso é pura programação orientada a serviços angular e javascript. Mas a integração herdada vem aqui:

<script type="text/javascript">

   (function(app){
        var conf = app.conf = {
       'fields': {
          'field1: { // field configuration }
        }
     } ; 

     app.dispatchEvent(new Event('register-schema'));

 })(window);
</script>

Obviamente, toda abordagem tem seus méritos e desvantagens. As vantagens e o uso dessa abordagem dependem da sua interface do usuário. As abordagens sugeridas anteriormente não funcionam no meu caso, pois meu esquema de formulário e código legado não têm controle e conhecimento de escopos angulares. Portanto, a configuração do meu aplicativo com base emangular.element('element-X').scope(); poderia potencialmente interromper o aplicativo se alterarmos os escopos. Mas se o seu aplicativo tem conhecimento do escopo e pode confiar que ele não muda com frequência, o sugerido anteriormente é uma abordagem viável.

Espero que isto ajude. Qualquer feedback também é bem-vindo.

Shakus
fonte