AngularJS - Scope in directive - angularjs

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/

Related

Angular 1.2: custom directive's dynamic content: ng-click doesn't work

I am trying to do a very simple thing in Angular 1.2: I want to create dynamic content for my custom directive, and add a click handler (clickCustomer) to parts of it. However, when I do that in the following pattern, whilst the clickCustomer function is available on the element's scope, it is not invoked when clicking on it. I'm gussing I need to get Angular to compile the dynamic content, but I'm not sure if that's actually the case, and if it is, how to do so.
'use strict';
angular.module('directives.customers')
.directive('customers', function () {
return {
restrict: 'A',
replace: true,
template: '<div class="customers"></div>',
controller: function ($scope, $element) {
var customers = ['Customer1', 'Customer2', 'Customer3'];
var customersMapped = customers.map(function (customer) {
return '<span ng-click="clickCustomer()" data-customer="' + customer + '">' + customer + '</span>';
});
var text = customersMapped.join(', ');
$element.html(text);
$scope.clickCustomer = function (event) {
console.log('Customer clicked', event);
}
}
};
});
You're right, you need to use the $compile service and compile the attached DOM elements so Angular will set up the events and scopes.
Check this fiddle.

Pass parameter for custom directive

Using Angular 1.5 with components.
Some parent HTML that contains custom directive:
<my-thing resetFields='$ctrl.bReset'></my-thing>
EDIT: instead of resetFields, here I should have used reset-fields - this was why I got the undefined below.
Parent controller:
function parentController() {
var ctrl = this;
ctrl.bReset= true;
}
Here is the component declaration for myThing:
alert(ctrl.reset); // alert is called in controller, but shows undefined
function myThingComponent() {
this.controller = {};
this.bindings = {};
var component = this;
component.templateUrl = 'myThing.html';
component.controller = myThingCtrl;
component.transclude = true;
component.bindings = {
resetFields: '<' // one way binding is needed
};
}
How can I send such parameter and use it in the custom directive's controller - myThingCtrl?
If the reset value is true I will perform some action and on false another action.
(Generally the question I guess is - how can I read a value from the parent inside the child component.)
For achive this propouse you have create a directive like below:
angular.module("yourModule")
.directive("myThing",function(){
return {
...
restrict : "E",
scope:{
reset:"=reset",
....
},
.....
}
}
});
in component way
angular.module('yourModule').component('myThing', {
...
bindings: {
reset: '='
}
});
The key point hear is use scope propertie like above and say that reset (reset in the left part) attribute in your directive is binded with a scope properties named reset (=reset rigth part) with the "=" you say that yoh have a two way data-binding.
I hope that this can help you
Here's an example from a simple directive I made that renders an animated counter.
function numberCounter($interval) {
var directive = {
restrict: 'E',
scope: {
start: '#start',
end: '#end',
speed: '#speed'
},
template: '<% number | number : 0 %>',
link: link
};
...
}
Then you can just use scope.start, scope.end, and scope.speed inside the link function. Start, end, and speed are attributes.

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!

Triggering a function with ngClick within ngTransclude

I have an unordered list loaded with four items from an array while using ngRepeat. The anchor tag in the list item has a function in the ngClick attribute that fires up a message. The function call works well when used like this:
<ul>
<li ng-repeat="n in supsNames">
<a ng-click="myAlert(n.name)">{{n.name}}</a>
</li>
</ul>
I created a simple directive for inserting unordered lists with list items. The list is loaded just fine but the same functioned I previously mentioned does not fire up. The code is as follows:
<div list items="supsNames">
<a ng-click="myAlert({{item.name}})">{{item.name}}</a>
</div>
Here is my javascript and angularjs code:
var app = angular.module('myapp', []);
app.controller('myCtrl', function($scope) {
$scope.title = 'ngClick within ngTransclude';
$scope.supsNames = [
{"name" : "Superman"},
{"name" : "Batman"},
{"name" : "Aquaman"},
{"name" : "Flash"}
];
$scope.myAlert = function(name) {
alert('Hello ' + name + '!');
};
});
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '='
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
console.log(scope);
}
};
});
I also have a plnkr in case you want to see what I tried to do:
http://plnkr.co/edit/ycaAUMggKZEsWaYjeSO9?p=preview
Thanks for any help.
I got the plunkr working. I'm not sure if its exactly what you're looking for. I copied the main code changes below.
Here's the plunkr:
http://plnkr.co/edit/GEiGBIMywkjWAaDMKFNq?p=preview
The modified directive looks like this now:
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '=',
ctrlFn: '&' //this function is defined on controller
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
//directive fn that calls controller defined function
scope.dirFn = function(param) {
if(scope.ctrlFn && typeof scope.ctrlFn == 'function') { //make sure its a defined function
scope.ctrlFn( {'name': param} ); //not sure why param has to be passed this way
}
}
}
};
});
And here's how it's called in the html file that's bound to your controller:
<div list items="supsNames" ctrl-fn="myAlert(name)">
<a ng-click="dirFn(item.name)">{{item.name}}</a>
</div>
I think what was happening before is that you were trying to use a function defined in your controller within the isolated scope of the directive, so it wasn't working--that function was undefined in the directive. So what I did was added another parameter to the directive that accepts method binding (I think that's what its called) with the '&'.
So basically you pass your controller method to the directive, and that method gets invoked however you want by the directive defined method I creatively named "dirFn". I don't know if this is the best way per se, but I've used it in an existing project with good results ;)
you need to pass the function to the directive
scope: {
items: '=', 'myAlert': '='
},
The ng-repeat inside the template of the directive insert a new scope and it require to call transclude funcion manually to work. I suggest remove ng-repeat and make the transclusion manually passing a copy of the controller scope and setting the item on each copy:
for(var i=0,len=scope.items.length;i<len;i++){
var item=scope.items[i];
var itemScope=scope.$parent.$new();
$transcludeFn(itemScope, function (clone,scope) {
// be sure elements are inserted
// into html before linking
scope.item=item;
element.after(clone);
});
};
I edit the pluker and I hope that could be helpfull: http://plnkr.co/edit/97ueb8SFj3Ljyvx1a8U1?p=preview
For more info about transclusion see: Transclusion: $transcludeFn

Scope problems within angular directives

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

Resources