17

Using scope: { ... } in a directive introduces an isolate scope, which does not prototypically inherit from its parent scope. But I have always used it for a different reason: a convenient way to declare HTML attributes with two way data binding:

scope: {
    attr1: '=',
    attr2: '?='
}

To get a non-isolate scope, you have to use scope: true, which does not offer the opportunity to declare such attributes. I now find myself needing a directive with a non-isolate scope, but with two way binding. What's the best way to achieve this?


Example: My use-case is something like this, in the view of the outer-directive:

<div ng-repeat="e in element">
    <inner-directive two-way-attr="e.value"></inner-directive>
</div>

But inner-directive is in the same module as outer-directive. It doesn't need to be encapsulated with an isolate scope. In fact, I need to use $scope inheritance for other purposes, so an isolate scope is not an option. It's just that using an HTML attribute to establish this two-way communication is extremely convenient.

mhelvens
  • 4,225
  • 4
  • 31
  • 55
  • 1
    When you are using `scope: true` you can directly access `scope` then whats the problem? _scope:true_ will create a child scope that will prototypically inherit from its parent, so you will be able to access values defined on parent's scope in your directive. – Satpal May 22 '14 at 10:20
  • https://github.com/angular/angular.js/wiki/Understanding-Scopes – Satpal May 22 '14 at 10:25
  • @Satpal: I'm aware of this, but for my purposes that is a rather inelegant way to establish communication. In fact, the link you just shared says: "it is considered bad form for two controllers to share information via $scope inheritance". --- I'll add a use-case example in my question. – mhelvens May 22 '14 at 12:14
  • @Satpal sometimes a component-like design for bindings is wanted (e.g. interface by which directive communicates) for directive meant to be used with other scope:true directives, but isolated scope can't be used due to illegal combination of new/isolated scope. – setec Feb 02 '17 at 14:00

4 Answers4

14

The answer by pixelbits helped me figure this out big time, but taken as a direct answer to my original question, it seems overly complicated. After looking into it, the solution is really quite simple.

Take a directive with an isolate scope like this:

scope: { model: '=myModel' },
link: function(scope, element, attr) {
    //...
}

The following is equivalent, except that the scope is not isolate:

scope: true,
link: function(scope, element, attr) {
    scope.model = scope.$parent.$eval(attr.myModel);
    //...
}

See a working example here: http://jsfiddle.net/mhelvens/SZ55R/1/

mhelvens
  • 4,225
  • 4
  • 31
  • 55
7

Working Demo Here

It is possible to have both a non-isolate scope and an isolate scope in the same directive. You might want to do this, for example, if you have a mix of both non-isolated templates (meaning they should not look for bindings through scope inheritance), and isolated templates (they should look for bindings in its own scope only) and they are both defined in the same directive.

In order to setup both isolate scope and non-isolate scope, you can do the following:

  1. In your directive definition, specify scope=true
  2. In your link function, compile and link your template against the scope parameter. When you do this, the bindings are evaluated against the non-isolate scope (meaning it resolves bindings through prototypical scope inheritance).

      link: function(scope, element, attr) {
    
        // this template should look for 'model' using scope inheritance
        var template2 = angular.element('<div> Prototypical Scope: {{ model }}</div>');
    
        // add the template to the DOM
        element.append(template2);
    
        // compile and link the template against the prototypical scope
        $compile(template2)(scope);
      }
    

    The advantage of prototypical scope inheritance is that you don't have to explicitly import bindings into your directives' current scope. As long as it is defined in the current scope or any scope higher up the inheritance chain (all the way up to the root scope), the angular run-time will be able to resolve it.

  3. In the same link function, define an isolated scope using scope.$new(true). You can establish a two-way binding of your model by importing a model into your isolated scope - isolatedScope.model = scope.$eval(attr.model):

     link: function(scope, element, attr) {
    
        // this template should look for 'model' in the current isolated scope only
        var template = angular.element('<div>Isolate Scope: {{model}}</div>');
    
        // create an isolate scope
        var isolatedScope = scope.$new(true);
    
        // import the model from the parent scope into your isolated scope. This establishes the two-way binding.            
        isolatedScope.model = scope.$eval(attr.model);
    
        // add the template to the DOM
        element.append(template);
    
        // compile and link the template against the isolate scope
        $compile(template)(isolatedScope);
    
    }
    

    The advantage of the isolate scope is that any bindings that exist (ie. are in-scope) are the ones that you explicitly import. Contrast this with non-isolate scope - where the bindings do not need to be explicitly defined on the current scope - it could be inherited from any scope higher up the chain.

Michael Kang
  • 52,003
  • 16
  • 103
  • 135
  • Thanks! This looks very promising. I'll have a try later today. – mhelvens May 24 '14 at 12:48
  • Thanks, your answer helped a lot. But the solution to my question actually appears to be a lot simpler. The key was your use of `scope.$eval`. To be honest, I'm not sure what the rest of your code is meant to accomplish, but I'd like to know. --- See my adapted solution in action here: http://jsfiddle.net/mhelvens/SZ55R/1/ – mhelvens May 24 '14 at 14:42
  • $eval here establishes not real dual-binding but just copies object reference, thus it will not work for non-object values (scalars, like string, number, object reference) – setec Feb 02 '17 at 14:17
6

I wrote this. You use it like this:

twowaybinder.attach($scope, $attrs.isDeactivated, 'isDeactivated');

.factory('twowaybinder', function ($parse) {
  function twoWayBind($scope, remote, local){
    var remoteSetter = $parse(remote).assign;
    var localSetter = $parse(local).assign;

    $scope.$parent.$watch(remote, function (value) {
      localSetter($scope, value);
    });

    $scope.$watch(local, function (value) {
      remoteSetter($scope, value);
    });
  }

  return {
    attach : twoWayBind
  };
});

It will give u true two-way binding from scope values. Note I dont think that $scope.$parent is neccessary, as in an inherited or no scope scenario any expression should resolve on the current scope. You would only need to call $parent in an isolated scope in which case you wouldn't use this, you would use the isolated scope config.

Sam
  • 1,725
  • 1
  • 17
  • 28
  • Have you tested this? I think this is on the right track but as written it will result in an infinite digest loop. If you look at the [angular source for $compile](https://github.com/angular/angular.js/blob/master/src/ng/compile.js) - search for "case: '='", you'll see they have introduced some extra logic to prevent this. Best answer of the group, though, IMHO. – Joe Enzminger Feb 26 '15 at 01:12
  • Yeah I am using it, but only in one instance so far. I haven't seen any infinite loopy type behaviour. I see what you mean though, if the remote value changes then the watcher will set the local value which will fire the local watcher and set the remote value and so on. However that doesn't seem to be happening. Perhaps something in $parse is stopping it. I'll have a closer look, thanks. – Sam Feb 26 '15 at 23:38
  • This solution is better than $eval solution, because it will work also for primitive values and object references. – setec Feb 02 '17 at 14:15
  • Note this is no longer needed. In 1.5 i think bindToController now accepts a hash in much the same way scope does and will bind the values to the controller complete with two way binding. You can leave scope set to false to true. – Sam Apr 11 '17 at 00:16
-3

you may use two directives if gg is an object then "=" points to one place of memory!

angular.module('mymodule', []).directive('a', function($parse, $modal) {
return {
    restrict : 'A',
    scope : {
        gg : "="
    },
    require : "b",
    link : function(scope, element, attrs, bCtrl) {
        scope.$watch('gg',function(gg){
            bCtrl.setset(scope.gg);
        }
    }
}
});

angular.module('mymodule').directive('b', function($parse, $modal) {

return {
    restrict : 'A',
    /*
     * scope : { showWarn : "=" },
     */
    controller : function($scope) {
        $scope.bb = {};

        this.setset = function(nn) {

            $scope.bb=nn;
        };

    }
});
olu
  • 220
  • 1
  • 2
  • 13
Amin Rahimi
  • 235
  • 3
  • 7