I have an issue where a directive with an isolate scope is not making changes via binding to its parent.parent scope.
In summary, I would expect that changes made to a property in the directive's isolate scope which is bound to a parent property would propagate all the way up parent scopes for that property (the property was inherited from a parent.parent scope to begin with), but it doesn't. It only changes the value on the immediate parent scope, while the parent.parent scope has the old value. Worse, any changes to the parent.parent scope property no longer trickle down to the isolate scope or its immediate parent. I understand this is the normal behavior of Javascript's prototype inheritance which Angular scopes are built on, but it's not wanted in this case of two-way data binding and I'm looking for a solution in Angular.
Here is an example of the behavior: http://jsfiddle.net/abeall/RmDuw/344/
My HTML contains a controller div (MyController1), in it is another controller div (MyController2), and in that is a directive (MyDirective):
<div ng-controller="MyController">
<p>MyController1: {{myMessage}} <button ng-click="change()">change</button></p>
<div ng-controller="MyController2">
<p>MyController2: {{myMessage}} <button ng-click="change()">change</button></p>
<p>MyDirective: <my-directive message="myMessage"/></p>
</div>
</div>
The Javascript defines myMessage on the outer MyController scope, the inner MyController2 scope inherits and binds myMessage to message on the directive, and the MyDirective defines message as an isolate scope property. At each level a change() function is defined which changes the local message property:
var app = angular.module('myApp', []);
function MyController($scope) {
var count = 0;
$scope.myMessage = "from controller";
$scope.change = function(){
$scope.myMessage = "from controller one " + (++count);
}
}
function MyController2($scope) {
var count = 0;
$scope.change = function(){
$scope.myMessage = "from controller two " + (++count);
}
}
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<span>Hello {{message}} <button ng-click="change()">change</button></span>',
scope: {
message: "="
},
link: function(scope, elm, attrs) {
var count = 0;
scope.change = function(){
scope.message = "from directive " + (++count);
}
}
};
});
What you'll notice is that:
If you click the MyController1's change button a few times, all levels are updated (it inherits downwards).
If you then click the MyDirective's change button, it updates MyController2 (binding upward) but does not change MyController1 in any way.
After this point, clicking MyController1's change button no longer trickles down to MyController2 and MyDirective scope, and vice versa. The two are now separated from each other. This is the problem.
So my question is:
Does Angular have a way to allow binding or inheritance of scope properties (myMessage in this case) to trickle all the way up parent scopes?
If not, in what way should I sync changes in a directive's isolate scope to a parent.parent controller scope's properties? The directive can not know about the structure of its parents.
as #Claies mentioned, using controller as is a great idea. This way you can be direct about which scope you want to update, as well as easily being able to pass scopes around in methods.
Here is a fiddle of the results you were likely expecting
Syntax: ng-controller="MyController as ctrl1"
Then inside: {{ctrl1.myMessage}} or ng-click="ctrl1.change()" and click="ctrl2.change(ctrl1)"
This changes the way you write your controller by leaving out the $scope dependency unless you need it for other reasons then holding your model.
function MyController () {
var count = 0
this.myMessage = "from controller"
this.change = function () {
this.myMessage = "from controller one " + (++count)
}
}
function MyController2 () {
var count = 0
this.change = function (ctrl) {
ctrl.myMessage = "from controller two " + (++count)
}
}
The easiest change is using $scope.msg.mymessage instead of just $scope.msg in your root controller.
function MyController($scope) {
var count = 0;
$scope.msg = {};
$scope.msg.myMessage = "from controller";
$scope.change = function(){
$scope.msg.myMessage = "from controller one " + (++count);
}
}
Here's a forked fiddle (that sounds funny) with the intended results.
http://jsfiddle.net/nk5cdrmx/
Related
In this plunk I have a directive that is instantiated twice. In each case, the result of the directive (as defined by its template) is displayed correctly.
Question is whether getValue2() needs to be defined as scope.getValue2 or var getValue2. When to use each in the directive?
HTML
instance 1 = <div dirx varx="1"></div>
<br/>
instance 2 = <div dirx varx="2"></div>
Javascript
var app = angular.module('app', []);
app.controller('myCtl', function($scope) {
});
app.directive('dirx', function () {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
varx: '='
};
directive.template = '{{getValue()}}';
directive.link = function (scope, element, attrs) {
scope.getValue = function(){
return getValue2();
};
var getValue2 = function() {
return scope.varx * 3;
}
};
return directive;
});
The only time you need declare something as a property on the $scope object is when it is part of your application state.
Angular 1.x will "dirty check" the $scope and make changes to the DOM. Anything on the $scope object can be watched, so you can observe the variable and trigger functions. This is why Angular searching & filtering can be done with almost no JS code at all. That being said, it's generally good practice to keep the '$scope' free of anything that isn't needed.
So as far as getValue() is concerned, is it being called on render or in a directive in your HTML? if the answer is "no", then it doesn't need to be declared as a property on the $scope object.
Since you're using getValue() in the directive template, it is being rendered in the UI and needs to be in Angular's $scope.
You can also just do:
directive.template = '{{ varx * 3 }}';
docs: https://docs.angularjs.org/guide/scope
The first thing is that the code contains unnecessary nested calls. it can be:
var getValue2 = function() {
return scope.varx * 3;
}
scope.getValue = getValue2;
The second thing is that getValue2 isn't reused and isn't needed, it can be:
scope.getValue = function() {
return scope.varx * 3;
}
Since getValue is used in template, it should be exposed to scope as scope.getValue. Even if it wouldn't be used in template, it's a good practice to expose functions to scope for testability. So if there's a real need for getValue2, defining and calling it as scope.getValue2 provides small overhead but improves testability.
Notice that the use of link function and direct access to scope object properties is an obsolete practice, while up-to-date approach involves controllerAs and this.
If I have two different directives with isolated scopes, and they share a controller, shouldn't they share the controller's $scope?
Effectively, I have the following scenario:
I call to the server and pass it some parameters, let's call them x and y.
The server either returns some data, or it returns null which indicates that this data is not available.
If the data is returned, I need to display a button; if the user clicks the button, it will display the data in a separate div.
If the data is not returned, I do not display the button.
I've been trying to implement this as a set of linked directives. I'm trying to do it this way because this "component" is to be re-used multiple times in the application, and I'm trying to do this in two directives because I couldn't figure out any other way to make a single directive control two different elements.
In addition to there being many, they need to be linked by x and y values. So if I have a button with particular x and y, then clicking on it will only toggle the visibility of the display area with the same x and y values. Effectively a given view will have multiple of each with different x and y values.
Anyway, I have a potential working solution, but I seem to be having problems with the shared scope. When I click the button, I have logging statements which correctly show that we trigger the "show" logic. But the logging statements in the ng-if of the div consistently evaluate the same logic to false and doesn't display.
My solution is in three parts:
A directive for the button
A directive for the "display"
A controller that is shared by the two
I have a trivial working example, which I will share below. There's a Plunkr URL at the end of this post as well.
Here is the HTML. The <p> tag in the middle is just to demonstrate that the two directives are physically not adjacent.
<trigger x="apple" y="2" ></trigger>
<p>Some unrelated dom content...</p>
<display x="apple" y="2"></display>
This is the trigger directive, which is the button:
app.directive("trigger", function() {
return {
restrict: "E",
scope: {
x : "#",
y : "#"
},
transclude: false,
template: "<button ng-if='hasCalculation(x,y)' ng-click='toggle()'>Trigger x={{x}} & y={{y}}</button>",
controller: 'testController',
link: function(scope) {
scope.doSomeWork();
}
};
});
This is the display directive, which is supposed to show the data when toggled by the button:
app.directive("display", function() {
return {
restrict: 'E',
scope: {
x : '#',
y : '#'
},
require: '^trigger',
transclude: false,
controller: 'testController',
template: "<p ng-if='shouldShow(x,y)'>{{getCalculation(x,y)}}</p>"
};
});
This is the shared controller, testController:
app.controller("testController", ["$scope", function($scope) {
$scope.shouldShow = [[]];
$scope.calculatedWork = [[]];
$scope.doSomeWork = function() {
var workResult = "We called the server and calculated something asynchonously for x=" + $scope.x + " and y=" + $scope.y;
if(!$scope.calculatedWork[$scope.x]) {
$scope.calculatedWork[$scope.x] = [];
}
$scope.calculatedWork[$scope.x][$scope.y] = workResult;
};
$scope.hasCalculation = function(myX, myY) {
var xRes = $scope.calculatedWork[myX];
if(!xRes) {
return false;
}
return $scope.calculatedWork[myX][myY]
}
$scope.toggle = function() {
if(!$scope.shouldShow[$scope.x]) {
$scope.shouldShow[$scope.x] = [];
}
$scope.shouldShow[$scope.x][$scope.y] = !$scope.shouldShow[$scope.x][$scope.y];
console.debug("Showing? " + $scope.shouldShow[$scope.x][$scope.y]);
}
$scope.isVisible = function(myX, myY) {
console.debug("Checking if we should show for " + myX + " and " + myY);
var willShow;
if(!$scope.shouldShow[myX]) {
willShow = false;
} else {
willShow = $scope.shouldShow[myX][myY];
}
console.debug("Will we show? " + willShow);
return willShow;
}
$scope.getCalculation = function(myX, myY) {
if(!$scope.calculatedWork[myX]) {
return null;
}
return $scope.calculatedWork[myX][myY];
}
}]);
Here is the Plunkr.
If you go to the Plunkr, you'll see the trigger button correctly rendered. If you click the button, you'll see that the toggle() method correctly flips the value of the shouldShow to the opposite of what it was previously (there's a console debug statement in that method that shows its result). You'll also see the re-evaluation of the isVisible method which determines if the display should show -- this always returns false.
I think it has to do with the fact that I'm saving my data and visibility state relative to $scope. My understanding of this is that each individual directive has its own $scope, but since they share a controller, shouldn't they share that controller's $scope?
An isolate scope does not prototypically inherit the properties of the parent scope.
In AngularJS, a child scope normally prototypically inherits from its parent scope. One exception to this rule is a directive that uses scope: { ... } -- this creates an "isolate" scope that does not prototypically inherit.(and directive with transclusion) This construct is often used when creating a "reusable component" directive. In directives, the parent scope is used directly by default, which means that whatever you change in your directive that comes from the parent scope will also change in the parent scope. If you set scope:true (instead of scope: { ... }), then prototypical inheritance will be used for that directive.
Source: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Additionally, a controller is not a singleton it is a class...so two directives can't share a controller. They each instantiate the controller.
If you want two controllers to share the same data, use a factory.
I am trying to implement a custom directive for a counter widget.
I have been able to implement it, but there are many things i need some light to be thrown on.
Can this directive be written in a better way ?
How do i use the scope:(isolate scope) in a better way ?
on click of any reset button i want all the startnumber to be reset to "1" ?
Where does the scope inherit from?Does it inherit from the element being called from?
HTML snippet
<body>
<counter-widget startnumber=1 ></counter-widget>
<counter-widget startnumber=1 ></counter-widget>
<counter-widget startnumber=1 ></counter-widget>
</body>
JS snippet
angular.module("myApp",[])
.directive("counterWidget",function(){
return{
restrict:"E",
scope:{
},
link:function(scope,elem,attr){
scope.f = attr.startnumber;
scope.add = function(){
scope.f = Number(scope.f) + 1;
}
scope.remove = function(){
scope.f =Number(scope.f) - 1;
}
scope.reset = function(){
scope.f = 1;
}
},
template:"<button ng-click='add()'>more</button>"+
"{{f}}"+
"<button ng-click='remove()'>less</button> "+
"<button ng-click='reset()'>reset</button><br><br>"
}
})
Thanks in advance for the help.
First, pass in your startnumber attribute, so we can reset to that number instead of having to hard code in a number.
You want to isolate the scope if you are going to have multiple counters.
But here is how you can implement a global reset:
app.directive("counterWidget",function(){
return{
restrict:"E",
scope:{
startnumber: '=',
resetter: '='
},
link:function(scope,elem,attr){
scope.f = attr.startnumber;
scope.add = function(){
scope.f++
}
scope.remove = function(){
scope.f--
}
scope.reset = function(){
scope.f = attr.startnumber
scope.$parent.triggerReset()
}
scope.$watch(function(attr) {
return attr.resetter
},
function(newVal) {
if (newVal === true) {
scope.f = attr.startnumber;
}
})
},
template:"<button ng-click='add()'>more</button>"+
"{{f}}"+
"<button ng-click='remove()'>less</button> "+
"<button ng-click='reset()'>reset</button><br><br>"
}
})
And in the controller you just add a small reset function that each directives is watching:
$scope.triggerReset = function () {
$scope.reset = true;
console.log('reset')
$timeout(function() {
$scope.reset = false;
},100)
}
I wouldn't overcomplicate the decrement and increment functions. ++ and -- should be fine.
We create the global reset function by adding an attribute and passing it in to the directive. We then watch that attribute for a true value. Whenever we click reset we trigger a function in the $parent scope (the triggerReset() function). That function toggles the $scope.reset value quickly. Any directive which has that binding in it's resetter attribute will be reset to whatever is in the startnumber attribute.
Another nice thing is that the reset will only affect counters you want it to. You could even create multiple groups of counters that only reset counters in it's own group. You just need to add a trigger function and variable for each group you want to have it's own reset.
Here is the demo:
Plunker
EDIT:
So the question came up in comments - can the $watch function 'miss' the toggle?
I did a little testing and the best answer I have so far is, on plunker if I set it to 1ms or even remove the time argument completely, the $watch still triggers.
I have also asked this question to the community here: Can $watch 'miss'?
You can use ng-repeat in your html.you can define count = 3
<body>
<div ng-repeat="index in count">
<counter-widget startnumber=1 ></counter-widget></div>
</body>
Also follow the link .They have explained scope inheritance in a better way
http://www.sitepoint.com/practical-guide-angularjs-directives-part-two/
Parent Scope (scope: false) – This is the default case. If your directive does not manipulate the parent scope properties you might not need a new scope. In this case, using the parent scope is okay.
Child Scope (scope:true) – This creates a new child scope for a directive which prototypically inherits from the parent scope. If the properties and functions you set on the scope are not relevant to other directives and the parent, you should probably create a new child scope. With this you also have all the scope properties and functions defined by the parent.
Isolated Scope (scope:{}) – This is like a sandbox! You need this if the directive you are going to build is self contained and reusable. Your directive might be creating many scope properties and functions which are meant for internal use, and should never be seen by the outside world. If this is the case, it’s better to have an isolated scope. The isolated scope, as expected, does not inherit the parent scope.
This will be a better approach.
HTML
<body>
<counter-widget counter="controllerScopeVariable" ></counter-widget>
</body>
JS
angular.module("myApp",[])
.directive("counterWidget",function(){
return{
restrict:"E",
scope:{
counter:'='
},
link:function(scope,elem,attr){
scope.counter = parseInt(scope.counter);
scope.add = function(){
scope.counter+=1;
}
scope.remove = function(){
scope.counter-=1;
}
scope.reset = function(){
scope.counter = 1;
}
},
template:"<button ng-click='add()'>more</button>"+
"{{f}}"+
"<button ng-click='remove()'>less</button> "+
"<button ng-click='reset()'>reset</button><br><br>"
}
});
I am creating a custom input directive for some added features.
app.directive('myTextInput', [
function () {
var directive = {};
directive.restrict = 'A';
directive.scope = {
textModel: '='
};
directive.link = function ($scope, element, attrs) {
// FUnctionality
};
directive.template = '<input ng-model="textModel" type="text">';
return directive;
}]);
and used the isolated scope property textModel for two way binding.
And the parent scope HTML is:
<div my-text-input text-model="myScopeVariable"></div>
<span>TYPED : {{myScopeVariable}}</span>
The controller scope has variable $scope.myScopeVariable=""; defined.
When i do this.
what ever typed in the directive input is able to print in the <span> tag.
Which tells it is updating.
The issue is.
in the parent scope. i have a method that will execute on a button click.
$scope.doSomething = function(){
console.log($scope.myScopeVariable);
};
On click on the button. the log is empty. which is supposed to be what is typed in the directive input.
THIS IS STRANGE
Now if define the scope variable as
$scope.myScopeVariable = {key:''};
and use in HTML as
<div my-text-input text-model="myScopeVariable.key"></div>
<span>TYPED : {{myScopeVariable.key}}</span>
then it is working every where. Even in the previously said function doSomething().
Any idea what happening here ?
This is happen because myScopeVariable contain value as String.
So if you change the value of from textbox of directive. It wont be reflected.
And in second method, you are referring Object. so whenever you change value of object , its relevant watch method is called. And value will be updated.
my-text-input is a directive, so he has his own scope, so the model myScopeVariable is not watch by the parent.
try with this :
<div my-text-input text-model="$parent.myScopeVariable"></div>
<span>TYPED : {{myScopeVariable}}</span>
It's not the best practice but it might works.
I have a directive with isolated scope with a value with two way binding to the parent scope. I am calling a method that changes the value in the parent scope, but the change is not applied in my directive.(two way binding is not triggered). This question is very similar:
AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
but I am not changing the value from the directive, but changing it only in the parent scope. I read the solution and in point five it is said:
The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't the parent's value is copied to the isolated scope.
Which means that when my parent value is changed to 2, a watch is triggered. It checks whether parent value and directive value are the same - and if not it copies to directive value. Ok but my directive value is still 1 ... What am I missing ?
html :
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{myValue}}</strong>
<span data-test-directive data-parent-item="myValue"
data-parent-update="update()"></span>
</div>
</div>
js:
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template:
'<button data-ng-click="lock()">Lock</button>' +
'</div>',
controller: function ($scope, $element, $attrs) {
$scope.lock = function () {
console.log('directive :', $scope.key);
$scope.parentUpdate();
//$timeout($scope.parentUpdate); // would work.
// expecting the value to be 2, but it is 1
console.log('directive :', $scope.key);
};
}
};
});
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
console.log('CTRL:', $scope.myValue);
};
});
Fiddle
Use $scope.$apply() after changing the $scope.myValue in your controller like:
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
$scope.$apply();
console.log('CTRL:', $scope.myValue);
};
});
The answer Use $scope.$apply() is completely incorrect.
The only way that I have seen to update the scope in your directive is like this:
angular.module('app')
.directive('navbar', function () {
return {
templateUrl: '../../views/navbar.html',
replace: 'true',
restrict: 'E',
scope: {
email: '='
},
link: function (scope, elem, attrs) {
scope.$on('userLoggedIn', function (event, args) {
scope.email = args.email;
});
scope.$on('userLoggedOut', function (event) {
scope.email = false;
console.log(newValue);
});
}
}
});
and emitting your events in the controller like this:
$rootScope.$broadcast('userLoggedIn', user);
This feels like such a hack I hope the angular gurus can see this post and provide a better answer, but as it is the accepted answer does not even work and just gives the error $digest already in progress
Using $apply() like the accepted answer can cause all sorts of bugs and potential performance hits as well. Settings up broadcasts and whatnot is a lot of work for this. I found the simple workaround just to use the standard timeout to trigger the event in the next cycle (which will be immediately because of the timeout). Surround the parentUpdate() call like so:
$timeout(function() {
$scope.parentUpdate();
});
Works perfectly for me. (note: 0ms is the default timeout time when not specified)
One thing most people forget is that you can't just declare an isolated scope with the object notation and expect parent scope properties to be bound. These bindings only work if attributes have been declared through which the binding 'magic' works. See for more information:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/
Instead of using $scope.$apply(), try using $scope.$applyAsync();