How to get two way data binding in a directive *without* an isolate scope?
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
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:
- In your directive definition, specify
scope=true
-
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.
-
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.
mhelvens
Computer Scientist, Software Engineer, Climber, Go-player, Geek, Skeptic
Updated on June 11, 2022Comments
-
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 asouter-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 about 10 yearsThanks! This looks very promising. I'll have a try later today.
-
mhelvens about 10 yearsThanks, 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 over 9 yearsHave 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 over 9 yearsYeah 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 over 7 yearsThis solution is better than $eval solution, because it will work also for primitive values and object references.
-
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 about 7 yearsNote 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 almost 7 yearsTo use non-objects models you should use parsing: attr="{{model}}" or attr="{{'string'}}"
-
Eduard Jacko almost 6 yearswhy not use the $parse here? $parse(attr.myModel)(this.$scope)