How to get two way data binding in a directive *without* an isolate scope?

11,571

Solution 1

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/

Solution 2

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.

Solution 3

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.

Share:
11,571
mhelvens
Author by

mhelvens

Computer Scientist, Software Engineer, Climber, Go-player, Geek, Skeptic

Updated on June 11, 2022

Comments

  • mhelvens
    mhelvens about 2 years

    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
    mhelvens about 10 years
    Thanks! This looks very promising. I'll have a try later today.
  • mhelvens
    mhelvens about 10 years
    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: jsfiddle.net/mhelvens/SZ55R/1
  • Joe Enzminger
    Joe Enzminger over 9 years
    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 - search for "case: '='", you'll see they have introduced some extra logic to prevent this. Best answer of the group, though, IMHO.
  • Sam
    Sam over 9 years
    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.
  • setec
    setec over 7 years
    This solution is better than $eval solution, because it will work also for primitive values and object references.
  • setec
    setec over 7 years
    $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)
  • Sam
    Sam about 7 years
    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.
  • LeonanCarvalho
    LeonanCarvalho almost 7 years
    To use non-objects models you should use parsing: attr="{{model}}" or attr="{{'string'}}"
  • Eduard Jacko
    Eduard Jacko almost 6 years
    why not use the $parse here? $parse(attr.myModel)(this.$scope)