AngularJs Directive: How to dynamically set attributes on ng-repeat element - javascript

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!

Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event

The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});

Related

Angular directives that call methods on child directives

I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.

AngularJS call method from an ancestor scope inside directive

I have an Angular app where I'm using ui-grid. I want to have a custom action on a cell of the grid that calls a method from my app. So basically, this means calling a method that's somewhere up in the parent hierarchy, from a directive.
This would be achieved by calling something like: $scope.$parent.$parent.$parent.$parent.foo(). But that doesn't seem too nice.
One option would be to create a recursive function that goes up the ancestry of the $scope. That's nicer, but still seems a bit weird.
Also... Is it good practice to try to achieve something like this?
You're correct that $parent.$parent.$parent is definitely not a good practice.
If the method you're calling is another directive, you can require that directive in your child directive and then, the parentDirective's controller function will be injected as the fourth parameter to your link function:
In your DDO:
return {
require : '^parentDirective',
restrict : 'E',
link : function (scope, elem, attrs, parentDirectiveController) {}
}
If what you're trying to call is on a factory/service, you can inject that factory/service into your directive, although this sometimes is a code smell, depending on what you're trying to inject.
Finally, another way to do it is to use event propagation. From your directive, you can use $scope.$emit to send information up to parent controllers:
From the directive:
$scope.$emit('directiveDidStuff', {
data : 'blah'
});
In the parent controller:
$scope.$on('directiveDidStuff', function (evt, params) {
this.data = params.data; // equals blah
});
You can achieve the same by using "&" through one of the scope variable in directive.Like this, you can bind your event to the controller method and from the method, you could do your desired things or if the original business logic which you wants to achieve on onClick of the grid is used across many modules than you can bisect it in service and make it reusable and call the service from the event method. Let me know if you do have any doubts with the approach.
Key Code of example:
Html
<my-component attribute-foo="{{foo}}" binding-foo="foo" isolated-expression- foo="updateFoo(newFoo)" >
Directive
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})

ui-select2 inside directive isn't updating controller model

I have a directive that takes in a collection and builds out a dropdown.
.directive("lookupdropdown", function () {
return {
restrict: 'E',
scope: {
collectionset: '=',
collectionchoice: '='
},
replace: true,
template: '<select class="input-large" ui-select2 ng-model="collectionchoice" data-placeholder="">' +
' <option ng-repeat="collection in repeatedCollection" value="{{collection.id}}">{{collection.description}}</option>' +
'</select>',
controller: ["$scope", function ($scope) {
$scope.repeatedCollection = new Array(); //declare our ng-repeat for the template
$scope.$watch('collectionset', function () {
if ($scope.collectionset.length > 0) {
angular.forEach($scope.collectionset, function (value, key) { //need to 'copy' these objects to our repeated collection array so we can template it out
$scope.repeatedCollection.push({ id: value[Object.keys(value)[0]], description: value[Object.keys(value)[1]] });
});
}
});
$scope.$watch('collectionchoice', function (newValue, oldValue) {
debugger;
$scope.collectionchoice;
});
} ]
}
});
This works fine. It builds out the drop down no problem. When I change the dropdown value, the second watch function gets called and I can see that it sets the value of collection choice to what I want. However, the collectionchoice that I have put into the directive doesn't bind to the new choice.
<lookupDropdown collectionset="SecurityLevels" collectionchoice="AddedSecurityLevel"></lookupDropdown>
That is the HTML markup.
This is the javascript:
$scope.SecurityLevels = new Array();
$scope.GetSecurityLevelData = function () {
genericResource.setupResource('/SecurityLevel/:action/:id', { action: "#action", id: "#id" });
genericResource.getResourecsList({ action: "GetAllSecurityLevels" }).then(function (data) {
$scope.AddedSecurityLevel = data[0].SCRTY_LVL_CD;
$scope.SecurityLevels = data;
//have to get security levels first, then we can manipulate the rest of the page
genericResource.setupResource('/UserRole/:action/:id', { action: "#action", id: "#id" });
$scope.GetUserRoles(1, "");
});
}
$scope.GetSecurityLevelData();
Then when I go to post my new user role, I set the user role field like this:
NewUserRole.SCRTY_LVL_CD = $scope.AddedSecurityLevel;
but this remains to be the first item EVEN though I have updated the dropdown, which according the watch function, it has changed to the correct value. What am I missing here?
You faced this issue because of the prototypical nature inheritance in Javascript. Let me try and explain. Everything is an object in Javascript and once you create an object, it inherits all the Object.Prototype(s), which eventually leads to the ultimate object i.e. Object. That is why we are able to .toString() every object in javascript (even functions) because they are all inherited from Object.
This particular issue on directives arises due to the misunderstanding of the $scope in Angular JS. $scope is not the model but it is a container of the models. See below for the correct and incorrect way of defining models on the $scope:
...
$scope.Username = "khan#gmail.com"; //Incorrect approach
$scope.Password = "thisisapassword";//Incorrect approach
...
$scope.Credentials = {
Username: "khan#gmail.com", //Correct approach
Password: "thisisapassword" //Correct approach
}
...
The two declarations make a lot of difference. When your directive updated its scope (isolated scope of directive), it actually over-rid the reference completely with the new value rather then updating the actual reference to the parent scope hence it disconnected the scope of the directive and the controller.
Your approach is as follows:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
The problem with this approach is that although it works, but it not the recommended solution and here is why. What if your directive is placed inside another directive with another isolated scope between scope of your directive and the actual controller, in that case you would have to do $parent.$parent.AddedSecurityLevel and this could go on forever. Hence NOT a recommended solution.
Conclusion:
Always make sure there is a object which defines the model on the scope and whenever you make use of isolate scopes or use ng directives which make use of isolate scopes i.e. ng-model just see if there is a dot(.) somewhere, if it is missing, you are probably doing things wrong.
The issue here was that my directive was being transcluded into another directive. Making the scope im passing in a child of the directive it was in. So something like $parent -> $child -> $child. This of course was making changes to the third layer and second layer. But the first layer had no idea what was going on. This fixed it:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>

Angular Directive to Directive call

If you have a directive that you're using multiple times on a page how can 1 directive communicate with another?
I'm trying to chain directives together in a parent child relationship. When directive A is clicked i want to filter Directive B to only have the children of the selected item in Directive A. In this case there may be infinite number of directives and relationships on the page.
Normally i would have Directive A call a filter method on each of it's children, and each child calls it's child to continue filtering down the hierarchy.
But i can't figure out if calling methods from 1 directive to another is possibe.
Thanks
It sounds like you are looking for a directive controller. You can use the require: parameter of a directive to pull in another directive's controller. It looks like this:
app.directive('foo', function() {
return {
restrict: 'A',
controller: function() {
this.qux = function() {
console.log("I'm from foo!");
};
},
link: function(scope, element, attrs) {
}
};
});
app.directive('bar', function() {
return {
restrict: 'A',
require: '^foo',
link: function(scope, element, attrs, foo) {
foo.qux();
}
};
});
From the angular docs, here are the symbols you can use with require and what they do.
(no prefix) - Locate the required controller on the current element.
? - Attempt to locate the required controller, or return null if not found.
^ - Locate the required controller by searching the element's parents.
?^ - Attempt to locate the required controller by searching the element's parents, or return null if not found.
Here's a jsbin of my example. http://jsbin.com/aLikEF/1/edit
Another option that may work for what you need is to have a service that each directive sets up a watch on and can manipulate. For example, directive1 may watch a property in the service and respond to changes and also setup a button that can change that property. Then, directive2 can also watch and change the service, and they will respond to one another however you set that up. If you need a jsbin of that also, just let me know.
I hope this helps!
You could try putting all of the data into a service that the directives can each reference.
Something like:
app.factory('selectedStuffService', function(){
var allItems = [];
var selectedItems = [];
function addSelectedItem(item){
selectedItems.push(item);
}
return {
allItems: allItems,
selectedItems: selectedItems,
addSelectedItem: addSelectedItem
}
}
Interactions in directive A change the values in the selectedItems array and directive B can bind to it. You can easily add other methods to the service to filter/manipulate the items as needed and any directive that uses the service should be able to update based on changes made by other directives.

accessing parent scope inside a directive

I have this two directives, one nested inside each other :
<envato class="container content-view-container" data-ng-cloak data-ng-hide="spinner">
<items data-ng-repeat="items in marketplaces"></items>
</envato>
And each of those two are defined as such :
Application.Envato.directive("envato", ["$timeout", function($timeout){
var object = {
restrict : "E",
controller : "EnvatoAPIController",
transclude : true,
replace : true,
templateUrl : "templates/envato-view.php",
link : function(scope, element, attrs, controller) {
console.log(scope);
return controller.getLatestItems().then(function(data) {
scope.marketplaces = angular.fromJson(data);
scope.count = scope.marketplaces.length;
var tst = angular.element(element).find(".thumbnails");
/* $timeout(function() { scope.swiper = new Swipe(document.getElementById('swiper-container')); }, 5000); */
scope.spinner = false;
});
}
};
return object;
}]);
Application.Envato.directive("items", function(){
var iterator = [],
object = {
require : "^envato",
restrict : "E",
transclude : false,
replace : true,
templateUrl : "templates/envato-items-view.php",
link : function(scope, element, attrs, controller) {
iterator.push(element);
if (iterator.length === scope.$parent.$parent.count) { console.log(iterator); };
}
};
return object;
});
A lot of the code above might not make a lot of sense because it's part of a bigger application, but I hope it does for my question. What I'm trying to do is to change a scope property of the directive envato from the directive items. Because I have a iteration and I want to know when it's done so I can do another operation on the appended DOM elements during that iteration.
For instance let's say I will have the scope.swipe defined inside the directive envato, and watch it for changes. In the directive items, I will watch when the ng-repeat is done and then change the above defined scope property scope.swipe. This will trigger the change inside the directive envato, and now I will know that I can do my operation.
I hope that I'm clear enough, if not I could try having more code or I'll try being more specific. How could I achieve what I just described above ?
EDIT : I do know that using : console.log(angular.element(element.parent()).scope()); inside the directive items will give me the scope of the envato directive, but I was wondering if there was a better way of doing it.
For this kind of inter-directive communication, I recommend defining an API/method on your envato directive that your items directive can call.
var EnvatoAPIController = function($scope) {
...
this.doSomething = function() { ... }
}
Your items directive already requires the envato directive, so in the link function of your items directive, just call the the API when appropriate:
require : "^envato",
link : function(scope, element, attrs, EnvatoCtrl) {
...
if(scope.$last) {
EnvatoCtrl.doSomething();
}
}
What is nice about this approach is that it will work even if you someday decide to use isolate scopes in your directives.
The tabs and pane directives on the AngularJS home page use this communication mechanism. See https://stackoverflow.com/a/14168699/215945 for more information. See also John's Directive to Directive Communication video.
Use scope.$eval('count') at item directive and let angular resolve for you.
I think you are looking for a callback that gets called when the ng-repeat completes. If that's what you want, i have created a fiddle. http://jsfiddle.net/wjFZR/.
There is no much of UI in the fiddle. Please open the firebug console, and run the fiddle again. You will see an log. That log is called at the end of an ng-repeat defined in the cell directive.
$scope.rowDone = function(){
console.log($scope)
} this is the callback function that is defined on the row directive that will get called when the ng-repeat of the cell directive is completed.
It is registered in this way.
<cell ng-repeat="data in rowData" repeat-done="rowDone()"></cell>
Disclaimer: I'm too a newbie in angularjs.
Hmmm it appears you are trying to make it difficult for yourself. In your directive you do not set a scope property:
var object = {
restrict : "E",
transclude : true,
replace : true,
scope: true,
...
Setting scope: {} will give your directive an fully isolated new scope.
BUT setting scope: true will give your directive a fully isolated new scope that inherits the parent.
I use this method to contain the model in the top level parent directive and allow it to filter down through all the child directives.
I love Mark's answer but I eventually created an attribute directive to save element directives' scopes to the rootScope like so:
myApp.directive('gScope', function(){
return {
restrict: 'A',
replace: false,
transclude: false,
controller: "DirectiveCntl",
link: function(scope, element, attrs, controller) {
controller.saveScope(attrs.gScope);
}
}
});
...
function DirectiveCntl($scope, $rootScope) {
this.saveScope = function(id) {
if($rootScope.directiveScope == undefined) {
$rootScope.directiveScope = [];
}
$rootScope.directiveScope[id] = $scope;
};
}
...
<span>Now I can access the message here: {{directiveScope['myScopeId'].message}}</span>
<other-directive>
<other-directive g-scope="myScopeId" ng-model="message"></other-directive>
</other-directive>
Note: While this makes it a snap to collect data from all your various directives it comes with my word of caution that now you have to ensure the potential pile of scopes are properly managed to avoid causing a memory leak on pages. Especially if you are using the ng-view to create a one page app.

Categories

Resources