181
votes

I'm using AngularJS to build HTML controls that interact with a legacy Flex application. All callbacks from the Flex app must be attached to the DOM window.

For example (in AS3)

ExternalInterface.call("save", data);

Will call

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

From within the JS resize function I'd like to dispatch an event that a controller can hear. It seems that creating a service is the way to go. Can you update a service from outside of AngularJS? Can a controller listen for events from a service? In one experiment (click for fiddle) I did it seems like I can access a service but updating the service's data doesn't get reflected in the view (in the example an <option> should be added to the <select>).

Thanks!

7
Note that in the jsfiddle above the injector is obtained without targeting an element within the app using var injector = angular.injector(['ng', 'MyApp']);. Doing this will give you a completely new context and a duplicate myService. That means you'll end up with two instances of the service and model and will be adding data to the wrong place. You should instead target an element within the app using angular.element('#ng-app').injector(['ng', 'MyApp']). At this point you can then use $apply to wrap model changes.Thanh Nguyen

7 Answers

296
votes

Interop from outside of angular to angular is same as debugging angular application or integrating with third party library.

For any DOM element you can do this:

  • angular.element(domElement).scope() to get the current scope for the element
  • angular.element(domElement).injector() to get the current app injector
  • angular.element(domElement).controller() to get a hold of the ng-controller instance.

From the injector you can get a hold of any service in angular application. Similarly from the scope you can invoke any methods which have been published to it.

Keep in mind that any changes to the angular model or any method invocations on the scope need to be wrapped in $apply() like this:

$scope.$apply(function(){
  // perform any model changes or method invocations here on angular app.
});
86
votes

Misko gave the correct answer (obviously), but some of us newbies may need it simplified further.

When if comes to calling AngularJS code from within legacy apps, think of the AngularJS code as a "micro app" existing within a protected container in your legacy application. You cannot make calls to it directly (for very good reason), but you can make remote calls by way of the $scope object.

To use the $scope object, you need to get the handle of $scope. Fortunately this is very easy to do.

You can use the id of any HTML element within your AngularJS "micro-app" HTML to get the handle of the AngularJS app $scope.

As an example, let's say we want to call a couple of functions within our AngularJS controller such as sayHi() and sayBye(). In the AngularJS HTML (view) we have a div with the id "MySuperAwesomeApp". You can use the following code, combined with jQuery to get the handle of $scope:

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

Now you can call your AngularJS code functions by way of the scope handle:

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

microappscope.sayHi();

microappscope.sayBye();

To make things more convenient, you can use a function to grab the scope handle anytime you want to access it:

function microappscope(){

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

}

Your calls would then look like this:

microappscope().sayHi();

microappscope().sayBye();

You can see a working example here:

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

I also showed this in a slideshow for the Ottawa AngularJS group (just skip to the last 2 slides)

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

24
votes

Greatest explanation of the concept I've found is situated here: https://groups.google.com/forum/#!msg/angular/kqFrwiysgpA/eB9mNbQzcHwJ

To save you the clicking:

// 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);
}); 
13
votes

Further to the other answers. If you don't want to access a method in a controller but want to access the service directly you can do something like this:

// 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 
13
votes

Thanks to the previous post, I can update my model with an asynchronous event.

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

I declare my model

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

And i update my model from outside my scope

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

More safe and performant way especially when debug data is off is to use a shared variable to hold a callback function. Your angular controller implements this function to return its internals to the external code.

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

Generalized as a reusable directive :

I created a 'exposeScope' directive which works in a similar fashion but usage is simpler:

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

This stores the current scope ( that is given to the link function of the directive) in a global 'scopes' object which is a holder for all scopes. Value provided to the directive attribute is used as the property name of the scope in this global object.

See the demo here. As I showed in the demo, you can trigger jQuery events when the scope is stored and removed from the global 'scopes' object.

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

Note that, I haven't tested the on('scopeDestroyed') when the actual element is removed from the DOM. If it does not work, triggering the event on the document itself instead of the element may help. ( see the app.js ) script in the demo plunker.

3
votes

I know this is an old question but I was looking at options to do this recently, so I thought I put my findings here in case it's useful to anyone.

In most cases, if there's the need for external legacy code to interact with the state of the UI or the inner workings of the application a service could be useful to abstract away those changes. If an external code is interacting directly with your angular controller, component or directive, you're coupling your app heavily with your legacy code which is bad news.

What I ended up using in my case, is a combination of browser accessible globals (i.e. window ) and event handling. My code has a smart form generation engine which requires JSON output from a CMS to initiliase the forms. Here's what I have done :

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

Form Schema Service is created using angular injector as expected:

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

And in my controllers: function () { 'use strict';

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

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

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

So far this is pure angular and javascript service oriented programming. But the legacy integration comes here :

<script type="text/javascript">

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

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

 })(window);
</script>

Obviously every approach has it's merits and drawbacks. The advantages and use of this approach depends on your UI. The previously suggested approaches don't work in my case since my form schema and legacy code have no control and knowledge of angular scopes. Hence configuring my app based on angular.element('element-X').scope(); could potentially break the app if we change the scopes around. But if you're app has knowledge of the scoping and can rely on it not changing often, what's suggested previously is a viable approach.

Hope this helps. Any feedback is also welcome.