Scope problems within angular directives - angularjs

I'm following the lessons on egghead.io (http://egghead.io/lessons/angularjs-directive-to-directive-communication), and I'm having some scope problems. When I mouse over <superhero flash>flash</superhero>, I am getting a blank array instead of 'speed'. However, when I add the flash directive to the second superhero directive, it prints it correctly. I am wondering if there are any scope problems I am having?
http://jsfiddle.net/9q6MG/
Console (on mouse over on flash)
Expected: 'speed'
Actual: []
http://jsfiddle.net/ewmCT/

The problem is because of the shared scope used by the superhero directive.
The superhero directive uses the parent elements scope as its own scope because you are not using child/isolated scopes for the directive. There for both the superhero elements in your sample shares the same scope.
So first superhero creates a empty array property abilities and since it has a speed directive adds speed to it, then when the second superhero element is compiled and processed it overrides this property again with a empty array because both of them works in the same scope
var app = angular.module('myApp', []);
app.directive('superhero', function () {
return {
restrict: 'E',
scope: true,
controller: function ($scope) {
$scope.abilities = [];
this.addStrength = function () {
console.log('adding strength', $scope.$id);
$scope.abilities.push('strength');
console.log($scope.abilities);
}
this.addSpeed = function () {
console.log('adding speed', $scope.$id);
$scope.abilities.push('speed');
console.log($scope.abilities);
}
},
link: function (scope, element) {
console.log('link', scope.$id, scope.abilities)
element.bind('mouseenter', function () {
console.log('me', scope.$id, scope.abilities)
})
}
}
})
Demo: Fiddle

Related

Angular event or command that signals the end of processing child directives

I'm currently coding a tab navigation example to gain practical experience with Angular. This example uses custom directives and controller inheritance.
Plunker can be found here.
The issue: once the directives have finished processing I'd like to select a default tab to display. But at the point of calling the selectTab method of the myTabs controller (line 41 in script.js $ctrl.selectTab(0)), Angular hasn't yet finished processing the myTab directive (which generates the tab links), so the tabs array is empty and the selection fails.
I tried using $timeout without a delay, but that fails. $timeout only works with a delay of say 500ms set, which is hacky.
Is there an event or command available that signals the end of Angular processing certain directives, particularly directives that inherit from one another?
What I suspect is happening:
The myTabs directive finishes processing then fires its link function, but it's fired before the myTab directive is done processing. I can't place the selectTab method call within the myTab link function because would get called multiple times (based on the number of tabs processed.) Hope I'm explaining this clearly...I need a massage
app.directive('myTabs', ['$timeout', function($timeout) {
return {
restrict: 'E',
transclude: true,
controllerAs: 'myTabsCtrl',
templateUrl: 'my-tabs.htm',
scope: {},
controller: function ($scope) {
vm = this;
this.tabs = [];
this.addTab = function (tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab (tabIndex) {
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i].selected = (i === tabIndex ? true : false);
}
};
},
link: function ($scope, $element, $attrs, $ctrl) {
$timeout(function () {
$ctrl.selectTab(0);
});
}
};
}]);
app.directive('myTab', function() {
return {
restrict: 'E',
require: '^^myTabs',
transclude: true,
templateUrl: 'my-tab.htm',
scope: {
title: '#'
},
link: function (scope, element, attrs, ctrl){
scope.tab = {
title: scope.title,
selected: false
};
ctrl.addTab(scope.tab);
}
};
});
Each individual tab is registering itself with the parent directive controller. Simply set the selected flag on the first tab when it registers itself:
this.tabs = [];
this.addTab = function (tab) {
console.log("add tab ", tab);
if (!this.tabs.length) {
//Set flag for first tab to register
tab.selected = true;
};
this.tabs.push(tab);
};
The DEMO on PLNKR
AngularJS 1.5.3 introduced the $postLink life-cycle hook:
Life-cycle hooks
Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the directive:
$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs.
--AngularJS $compile Service API Reference -- Life-cycle hooks
The $postLink life-cycle hook won't work in this case because the directives use templateUrl. It also won't work with directives that build DOM after postlink; ng-repeat, ng-if, ng-include, etc.

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding

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();

Controller Required By Directive Can't Be Found

I have a directive that I'd like another directive to be able to call in to. I have been trying to use directive controllers to try to achieve this.
Directive one would be sitting on the same page as directive two, and directive one would call methods exposed by directive two's controller:
Directive 1:
'use strict';
angular.module('angularTestApp')
.directive('fileLibrary', function () {
return {
templateUrl: 'views/manage/file_library/file-library.html',
require: 'videoClipDetails',
restrict: 'AE',
link: function postLink(scope, element, attrs, videClipDetailsCtrl) {
scope.doSomethingInVideoClipDirective = function() {
videClipDetailsCtrl.doSomething();
}
}
};
});
Directive Two:
'use strict';
angular.module('angularTestApp')
.directive('videoClipDetails', function () {
return {
templateUrl: 'views/video_clip/video-clip-details.html',
restrict: 'AE',
controller: function($scope, $element) {
this.doSomething = function() {
console.log('I did something');
}
},
link: function postLink(scope, element, attrs) {
console.log('videoClipDetails directive');
//start the element out as hidden
}
};
});
File where the two are used and set up as siblings:
<div>
<div video-clip-details></div>
<!-- main component for the file library -->
<div file-library></div>
</div>
I know reading documentation I picked up that the controllers can be shared when the directives are on the same element, which makes me think I might be looking at this problem the wrong way. Can anyone put me on the right track?
From the angular.js documentation on directives
When a directive uses require, $compile will throw an error unless the specified controller is found. The ^ prefix means that this directive searches for the controller on its parents (without the ^ prefix, the directive would look for the controller on just its own element).
So basically what you are trying to do with having siblings directly communicate is not possible. I had run into this same issue but I did not want to use a service for communication. What I came up with was a method of using a parent directive to manage communication between its children, which are siblings. I posted the example on github.
What happens is that both children require the parent (require: '^parentDirective') and their own controller, both of which are passed into the link function. From there each child can get a reference to the parent controller and all of its public methods, as an API of sorts.
Below is one of the children itemEditor
function itemEditor() {
var directive = {
link: link,
scope: {},
controller: controller,
controllerAs: 'vm',
require: ['^itemManager', 'itemEditor'],
templateUrl: 'app/scripts/itemManager/itemManager.directives.itemEditor.html',
restrict: 'A'
};
return directive;
function link(scope, element, attrs, controllers) {
var itemManagerController = controllers[0];
var itemEditorController = controllers[1];
itemEditorController.itemManager = itemManagerController;
itemEditorController.initialize();
}
function controller() {
var vm = this;
// Properties
vm.itemManager = {};
vm.item = { id: -1, name: "", size: "" };
// Methods
vm.initialize = initialize;
vm.updateItem = updateItem;
vm.editItem = editItem;
// Functions
function initialize() {
vm.itemManager.respondToEditsWith(vm.editItem);
}
function updateItem() {
vm.itemManager.updateItem(vm.item);
vm.item = {};
}
function editItem(item) {
vm.item.id = item.id;
vm.item.name = item.name;
vm.item.size = item.size;
}
}
}
Note how the values passed into the require array are the parent directive's name and the current directive's name. These are then both accessible in the link function via the controllers parameter. Assign the parent directive's controller as a property of the current child's and then it can be accessed within the child's controller functions via that property.
Also notice how in the child directive's link function I call an initialize function from the child's controller. This is where part of the communication lines are established.
I'm basically saying, anytime you (parent directive) receive a request to edit an item, use this method of mine named editItem which takes an item as a parameter.
Here is the parent directive
function itemManager() {
var directive = {
link: link,
controller: controller,
controllerAs: 'vm',
templateUrl: 'app/scripts/itemManager/itemManager.directives.itemManager.html',
restrict: 'A'
};
return directive;
function link(scope, element, attrs, controller) {
}
function controller() {
var vm = this;
vm.updateMethod = null;
vm.editMethod = null;
vm.updateItem = updateItem;
vm.editItem = editItem;
vm.respondToUpdatesWith = respondToUpdatesWith;
vm.respondToEditsWith = respondToEditsWith;
function updateItem(item) {
vm.updateMethod(item);
}
function editItem(item) {
vm.editMethod(item);
}
function respondToUpdatesWith(method) {
vm.updateMethod = method;
}
function respondToEditsWith(method) {
vm.editMethod = method;
}
}
}
Here in the parent you can see that the respondToEditsWith takes a method as a parameter and assigns that value to its editMethod property. This property is called whenever the controller's editItem method is called and the item object is passed on to it, thus calling the child directive's editItem method. Likewise, saving data works the same way in reverse.
Update: By the way, here is a blog post on coderwall.com where I got the original idea with good examples of require and controller options in directives. That said, his recommended syntax for the last example in that post did not work for me, which is why I created the example I reference above.
There is no real way with require to communicate between sibling elements in the way you are trying to do here. The require works the way you have set up if the two directives are on the same element.
You can't do this however because both of your directives have an associated templateUrl that you want to use, and you can only have one per element.
You could structure your html slightly differently to allow this to work though. You basically need to put one directive inside the other (transcluded) and use require: '^videoClipDetails'. Meaning that it will look to the parent to find it.
I've set up a fiddle to demonstrate this: http://jsfiddle.net/WwCvQ/1/
This is the code that makes the parent thing work:
// In videoClipDetails
template: '<div>clip details<div ng-transclude></div></div>',
transclude: 'true',
...
// in markup
<div video-clip-details>
<div file-library></div>
</div>
// in fileLibrary
require: '^videoClipDetails',
let me know if you have any questions!

AngularJS - Scope in directive

I'm new to AngularJS.
Can someone explain me why the active class not toggle between tabs in this code: http://jsfiddle.net/eSe2y/1/?
angular.module('myApp', [])
.filter('split', function () {
return function (input, string) {
var temp = string.split('|');
for (var i in temp)
input.push(temp[i]);
return input;
};
})
.directive('myTabs', function () {
return {
restrict: 'E',
scope: { tabs: '#' },
template:
"<div>" +
"<a ng-repeat='e in [] | split:tabs' ng-click='selectedIndex = $index' ng-class='{active:$index==selectedIndex}'>{{e}}</a>" +
"</div>",
replace: true
}
});
If I move the ng-click expression to a method of the controller, the code works as expected: http://jsfiddle.net/g36DY/1/.
angular.module('myApp', [])
.filter('split', function () {
return function (input, string) {
var temp = string.split('|');
for (var i in temp)
input.push(temp[i]);
return input;
};
})
.directive('myTabs', function () {
return {
restrict: 'E',
scope: { tabs: '#' },
template:
"<div>" +
"<a ng-repeat='e in [] | split:tabs' ng-click='onSelect($index)' ng-class='{active:$index==selectedIndex}'>{{e}}</a>" +
"</div>",
replace: true,
controller: ['$scope', function ($scope) {
$scope.onSelect = function (index) {
$scope.selectedIndex = index;
}
}]
}
});
Can someone explain me the difference? And how to modify the first code to make it works without create a method to the controller?
Thanks in advance.
Explanation of the Problem
The problem has to do with javascript inheritance as it relates to scopes and directives in angular. Basically, when in a child scope, all properties of basic types (int, boolean, etc) are copied from the parent.
In your case, the ng-repeat directive creates a child scope for each element, so each one of the links has its own scope. in the first example, selectedIndex is only referenced from within the repeater, each repeater element references its own copy of the selectedIndex. You can investigate this using the
In the second example, you define a selectedIndex object in the controller, which is the parent scope for the repeater. Because the selectedIndex property is undefined initially when it is passed into the controllers, they look to the parent for a definition. When this definition has a value set in the onSelect method, all of the repeater elements "see" this value, and update accordingly.
How to Debug
In the future, you can investigate these types issue using the Angular Batarang.
browse to http://jsfiddle.net/eSe2y/1/show
left-click one of the tabs
right-click the same link
select "inspect element"
open the debug console and type $scope.selectedIndex
repeat the above steps for another tab, and note how the value differs
now go to the elements tab of the debugger, and click on the div
enter $scope.selectedIndex and note that it is undefined
On the second fiddle, try viewing just the $scope on each of the tabs (not $scope.selectedIndex). You will see that selectedIndex is not defined on the repeater elements, so they default to the value from their parent.
Best Practices
The typical angular best practice to avoid this problem is to always reference items that could be change on the scope "after the dot". This takes advantage of the fact that JavaScript objects are inherited by reference, so when the property changes in one place, it changes for all scopes in the parent-child hierarchy. I've posted an updated fiddler that fixes the problem by simply pushing the binding onto an object:
angular.module('myApp', [])
.filter('split', function () {
return function (input, string) {
var temp = string.split('|');
for (var i in temp)
input.push(temp[i]);
return input;
};
})
.directive('myTabs', function () {
return {
restrict: 'E',
scope: { tabs: '#' },
template:
"<div>" +
"<a ng-repeat='e in [] | split:tabs' ng-click='s.selectedIndex = $index' ng-class='{active:$index==s.selectedIndex}'>{{e}}</a>" +
"</div>",
replace: true,
controller: ['$scope', function ($scope) {
$scope.s = {};
}]
}
});
http://jsfiddle.net/g36DY/2/

Resources