Reputation: 29221
I'd like to have a one-way (not one time) binding between an attribute on a directive, but i'm struggling with how to express this without attrs.$observe
. The best I can come up with at the moment is to bind via &attr
and invoke the variables I am binding to in my template e.g. {{attr()}}
app.controller('MainCtrl', function($scope) {
$scope.names = ['Original'];
setTimeout(function () {
$scope.names.push('Asynchronously updated name');
$scope.$apply();
}, 1000);
});
app.directive('helloComponent', function () {
return {
scope: {
'names': '&names'
},
template: '<li ng-repeat="name in names()">Hello {{name}}</li>'
}
});
<body ng-controller="MainCtrl">
<ul>
<hello-component names="names"/>
</ul>
</body>
Is there a better way to do this that preserves the one-way binding without the need to invoke the bound properties?
I've updated the example code to clarify that I want to bind to an object, not just a string. So @attr
(which works with a string attribute) is not a solution.
Upvotes: 3
Views: 10760
Reputation: 2734
I didn't see any mention of it in the other answers, but as of Angular 1.5, one-way bindings for objects are supported (see scope
section in $compile docs for Angular 1.5.9):
<
or<attr
- set up a one-way (one-directional) binding between a local scope property and an expression passed via the attributeattr
. The expression is evaluated in the context of the parent scope. If noattr
name is specified then the attribute name is assumed to be the same as the local name. You can also make the binding optional by adding?
:<?
or<?attr
.For example, given
<my-component my-attr="parentModel">
and directive definition ofscope: { localModel:'<myAttr' }
, then the isolated scope propertylocalModel
will reflect the value ofparentModel
on the parent scope. Any changes toparentModel
will be reflected inlocalModel
, but changes inlocalModel
will not reflect inparentModel
. There are however two caveats:
- one-way binding does not copy the value from the parent to the isolate scope, it simply sets the same value. That means if your bound value is an object, changes to its properties in the isolated scope will be reflected in the parent scope (because both reference the same object).
- one-way binding watches changes to the identity of the parent value. That means the
$watch
on the parent value only fires if the reference to the value has changed. In most cases, this should not be of concern, but can be important to know if you one-way bind to an object, and then replace that object in the isolated scope. If you now change a property of the object in your parent scope, the change will not be propagated to the isolated scope, because the identity of the object on the parent scope has not changed. Instead you must assign a new object.One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings back to the parent. However, it does not make this completely impossible.
In the example below, one-way binding is used to propagate changes in an object in the scope of a controller to a directive.
As pointed out by @Suamere, you can indeed change properties of a bound object with one-way-binding; however, if the whole object is changed from the local model, then the binding with the parent model will break, as the parent and local scope will be referring to different objects. Two-way binding takes care of this. The code snippet was updated to highlight the differences.
angular.module('App', [])
.directive('counter', function() {
return {
templateUrl: 'counter.html',
restrict: 'E',
scope: {
obj1: '<objOneWayBinding',
obj2: '=objTwoWayBinding'
},
link: function(scope) {
scope.increment1 = function() {
scope.obj1.counter++;
};
scope.increment2 = function() {
scope.obj2.counter++;
};
scope.reset1 = function() {
scope.obj1 = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "One-way binding",
creator: "Directive"
};
};
scope.reset2 = function() {
scope.obj2 = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "Two-way binding",
creator: "Directive"
};
};
}
};
})
.controller('MyCtrl', ['$scope', function($scope) {
$scope.increment = function() {
$scope.obj1FromController.counter++;
$scope.obj2FromController.counter++;
};
$scope.reset = function() {
$scope.obj1FromController = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "One-way binding",
creator: "Parent"
};
$scope.obj2FromController = {
counter: 0,
id: Math.floor(Math.random()*10000),
descr: "Two-way binding",
creator: "Parent"
};
};
$scope.reset();
}])
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.js"></script>
<div ng-app="App">
<script type="text/ng-template" id="counter.html">
<h3>In Directive</h3>
<pre>{{obj1 | json:0}}</pre>
<button ng-click="increment1()">
Increment obj1 from directive
</button>
<button ng-click="reset1()">
Replace obj1 from directive (breaks binding)
</button>
<pre>{{obj2 | json:0}}</pre>
<button ng-click="increment2()">
Increment obj2 from directive
</button>
<button ng-click="reset2()">
Replace obj2 from directive (maintains binding)
</button>
</script>
<div ng-controller="MyCtrl">
<counter obj-one-way-binding="obj1FromController"
obj-two-way-binding="obj2FromController">
</counter>
<h3>In Parent</h3>
<pre>{{obj1FromController | json:0}}</pre>
<pre>{{obj2FromController | json:0}}</pre>
<button ng-click="increment()">
Increment from parent
</button>
<button ng-click="reset()">
Replace from parent (maintains binding)
</button>
</div>
</div>
Upvotes: 3
Reputation: 310
This may be essentially the same approach proposed by New Dev, but I solved a similar problem for myself by taking an object off of my isolate scope and creating a getter function for it which called scope.$parent.$eval(attrs.myObj)
.
In a simplified version that looks more like yours I changed:
app.directive('myDir', [function() {
return {
scope : {
id : '@',
otherScopeVar : '=',
names : '='
},
template : '<li ng-repeat="name in names">{{name}}</li>'
}
}]);
to
app.directive('myDir', [function() {
return {
scope : {
id : '@',
otherScopeVar : '='
},
template : '<li ng-repeat="name in getNames()">{{name}}</li>',
link : function(scope, elem, attrs) {
scope.getNames() {
return scope.$parent.$eval(attrs.myList);
};
}
}
}]);
That way whenever a digest runs your object is pulled as is from the parent scope. For me, the advantage to doing it this way was that I was able to change the directive from two-way to one-way binding (which took my performance from unusable to working fine) without changing the views that used the directive.
EDIT
On second thought I am not sure this is exactly one-way binding, because while updating the variable and running a digest will always use the updated object, there is no inherent way to run other logic when it changes, as one could with a $watch
.
Upvotes: -2
Reputation: 49590
The "&"
is actually the right thing to do. I have argued against this approach (with @JoeEnzminger, here and here) on the basis that it is semantically questionable. But overall Joe was right - this is the way to create a one-way binding to an actual object vs. "@"
which binds to a string.
If you don't fancy an isolate scope, then you could get the same effect by using $parse
:
var parsedName = $parse(attrs.name);
$scope.nameFn = function(){
return parsedName($scope);
}
and use it in the template as:
"<p>Hello {{nameFn()}}</p>"
Upvotes: 5
Reputation: 25159
Doing an attribute literally passes a string. So instead of doing this:
<hello-component name="name"/>
You can do this:
<hello-component name="{{name}}"/>
Upvotes: 0