7

I’m trying to understand interactions between the Angular world and the non-Angular world.

Given a directive that one declares like this:

<dir1 id="d1" attr1="100"/>

If code outside angular changes the directive this way:

$("#d1").attr("attr1", 1000);

How can the directive know that one of its attribute has changed?

Dan
  • 59,490
  • 13
  • 101
  • 110
Sylvain
  • 19,099
  • 23
  • 96
  • 145
  • Are you the creator of said code? Or is this an external application changing your DOM? [`Scope.$apply`](http://docs.angularjs.org/api/ng.$rootScope.Scope#$apply) may be what you're after. More than likely though, you'll want to hand your data to your Angular code and let Angular manage the DOM changes rather than the other way around. – Travis Watson Jun 18 '13 at 02:36

2 Answers2

8

It would be best to make this change inside the directive instead. If, for whatever reason, that's not possible, then there are a couple of options.

Outside the app, get a reference to any DOM element within the app. Using that reference, you can then get a reference to its scope. You could use your element with id d1. For example:

var domElement = document.getElementById('d1');
var scope = angular.element(domElement).scope();

Here are a couple of options:

Option 1

Modify the model instead of making a direct change to the view. In the link function, store the initial attribute value in a scope variable like:

scope.myvalue = attrs.attr1;

Then you can change the value outside the app (using the above reference to scope) like:

scope.$apply(function(){
    scope.myvalue = 1000;
    console.log('attribute changed');
});

Here is a fiddle

Option 2

If the view is manipulated directly with jQuery, I don't know of any use of $observe, $watch, or an isolate scope binding to the attribute that will work, because they all bind to the attribute expression itself, just once, when the link function is first run. Changing the value will cause those bindings to fail. So you'd have to $watch the attribute on the DOM element itself (rather than through attrs):

scope.$watch(function(){         
    return $(el).attr('attr1');    // Set a watch on the actual DOM value
}, function(newVal){
    scope.message = newVal;
});

Then you can change the value outside the app (using the above reference to scope) like:

scope.$apply(function(){
    $("#d1").attr("attr1",1000);
});

Here is a fiddle

Community
  • 1
  • 1
Dan
  • 59,490
  • 13
  • 101
  • 110
  • See http://plnkr.co/edit/ESrh2v1AXBMKOGncg8TS?p=preview. I tried $apply() but I can't get it to work. – Sylvain Jun 18 '13 at 15:46
  • This is actually unrelated; the plunk isn't working because the `$observe` doesn't fire even if the attribute is changed from within the directive. The reason is somewhat hidden in the angular source: "The observer will never be called, if given attribute is not interpolated." (i.e. if it contains no brackets "{{}}"). Try creating a binding in isolate scope and using a `$watch` instead. – Dan Jun 18 '13 at 19:37
  • See this new plunk using `$watch()`. Is that what you meant? It does not work either. http://plnkr.co/edit/uSdbISm3WoNSi7e2ODxQ?p=preview – Sylvain Jun 18 '13 at 20:08
  • The `$watch` needs to be on a variable, right now it's on the literal '100' (which does not change, but only gets overridden). Also, notice the manual check did not work in either plunk. I suggest using a variable to store the original value and to change the variable directly with your jQuery. [Here is a demo](http://jsfiddle.net/sh0ber/rg8rL/) – Dan Jun 18 '13 at 20:45
  • Would you then say that there's no way for a directive to be notified that one of its attribute has changed through DOM manipulations? – Sylvain Jun 18 '13 at 22:31
  • It's doable, but seems kind of hacky to me. I updated my answer with a 2nd option to shows how it could be done. – Dan Jun 19 '13 at 01:32
  • Thanks for taking the time to test all this and come up with this very good answer. Option 2 is the closest to what I was trying to demonstrate. – Sylvain Jun 19 '13 at 17:16
  • using .scope() only works when debug mode is on. in most production apps debug mode will be turned off and using .scope() will give you errors @sh0ber – trickpatty Jan 21 '16 at 14:58
  • @PatrickLawler Correct, debug mode should be on when using `scope()`. – Dan Jan 21 '16 at 15:55
1

Use a Web Components library like x-tags by Mozilla or Polymer by Google. This option works without maunally calling $scope.$apply every time the attribute changes.

I use x-tags because of their wider browser support. While defining a new custom tag (directive) you can set the option lifecycle.attributeChanged to a callback function, which will fire every time an argument is changed.

The official docs aren't very helpful. But by trial and error and diving into the code I managed to find out how it works.

The callback function's context (the this object) is the element itself. The one whose attribute has changed. The callback can take three arguments:

  • name – the name of the attribute,
  • oldValue and
  • newValue – these speak for themselves.

So now, down to business:

The code

This will watch the attribute for changes:

xtag.register('dir1', {
    lifecycle: {
        attributeChanged: function (attribute, changedFrom, changedTo) {
            // Find the element's scope
            var scope = angular.element(this).scope();

            // Update the scope if our attribute has changed
            scope.$apply(function () {
                if (attribute == 'attr1') scope.style = changedTo;
            });
        }
    }
});

The attributeChanged callback only fires when the arguments' values actually change. To get their initial values you need to scan the lot manually. The easiest way seems to be while defining the directive:

myApp.directive('dir1', function () {
    return {
        ... ,
        link: function (scope, element, attributes) {
            scope.attr1 = element[0].getAttribute('attr1');
        }
    };
});
Community
  • 1
  • 1
tomekwi
  • 2,048
  • 2
  • 21
  • 27