How to re-render a directive template and link function? - angularjs

I am creating a navigation "tree" that basically allows you to navigate to parent views like so
Home > Planner > Contacts > john.smith#hotmail.com
This list is just a normal <ul> that should get changed each time the view is changed and allow the user to click on any of the above links to navigate. I am using ui-router's $stateChangeStart event to build up my list of links, the problem is that my directive does not "re-build" this list every time the page change.
Is there a way to force the directive to re-render every time I change my state?
TreeNav directive
app.directive('treeNav', ['$rootScope', function ($rootScope) {
return {
replace: true,
restrict: 'E',
template: '<ul>\
<li ng-repeat="item in treeNavs">\
<a ui-sref="{{ item.state }}">{{ item.name }}</a>\
<span> > </span>\
</li>\
</ul>',
link: function (scope, elem, attrs) {
scope.treeNavs = $rootScope.treeNav.reverse();
scope.$watch('$rootScope.treeNav', function (newVal, oldVal) {
scope.treeNavs = newVal.reverse();
});
}
}
}]);

I believe you aren't setting up your $watch statement correctly.
You should also be careful with your use of .reverse. reverse() reverses the entries in an array in place, which is probably not what you want to do, since it would modify the array on the $rootScope.
You can use splice() to create a new copy and then reverse that:
scope.treeNavs = $rootScope.treeNavs.splice().reverse();
$rootScope.$watch('treeNav', function(newVal) {
scope.treeNavs = newVal.splice().reverse();
});

Related

pass data from controller to directive's link?

In my controller :
myApp.controller('homeCtrl', function($scope, $rootScope, $state, 'red';
$rootScope.$on('new_story', function(event, data) {
$scope.cardObj = {key:'value'};
});
});
In my HTML :
<div clickmeee ></div>
<div id="feedContainer" card='{{cardObj}}'> </div>
In my directive :
myApp.directive('clickmeee', function($compile, $rootScope) {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
element.bind('click', function() {   
scope.$watch('card', function(newVal, oldVal) {
alert(scope.card);
});       
});
}
};
});
How do I pass data from controller to this directive. I compile some html and prepend it to the div. All of that is sorted out but I need some data from object I am trying to pass.
Any help??
There are several problems in your code:
you define a scope attribute named 'card', but you use cardObj instead
you use a watch that is completely unnecessary. And worse: you create a new watch every time the element is clicked
you don't define any card attribute on your clickmeee element. Instead, you're placing it on another element, on which the directive is not applied
you're passing the attribute with '#'. That works, but the directive will receive a string, containing the JSONified object, rather than the object itself
you're not showming us where you emit an event that will initialize cardObj in the controller scope
Here is a plunkr showing a working version of your code.
Also, note that using bind('click') is a bad idea. You'd better have a template in your directive and use ng-click in the template, or simply not use a directive at all and just use ng-click directly on the div element.
Bad news. You are doing it wrong all the ways.
Firstly
card='{{cardObj}}' >
this one should be put in the
<div clickmeee ></div>
So you can take it as binded scope variable in your directive registration
Secondly
If you managed to use '#' syntax
card: '#'
it will turn your input to string, not a binded scope. Use '=' instead.
In the end
You dont need to use watch here:
scope.$watch('card', function(newVal, oldVal) {
alert(newVal);
});
since scope.card is binded via '=' connector. Just simple use alert(scope.card). (Need to warn you that alert an object is not a good idea)
I have tried your code here: plunker. Changed a litte bit by using cardObj as string for easier presentation. Does it match your work?
You should watch the card object:
myApp.directive('clickmeee', function() {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
scope.$watch('card', function(value) {
console.log(value);
});
}
};
});
And:
<div clickmeee id="feedContainer" card='{{cardObj}}'> </div>
Whenever the controller changes the cardObj, the directive's watch on card is triggered:
$scope.$apply(function() {
$scope.cardObj = "test";
}

Recalculate/Reinitialise directive - after async data fetch

I have a directive kind of like this:
function timeDisplay() {
var directive = {
restrict: "E",
link: function (scope, element, attrs) {
scope.tooltip = scope.minutes > 60 ? (scope.minutes / 60) + " hrs" : scope.minutes + " mins";
},
replace: true,
template: "<abbr uib-tooltip='{{ tooltip }}'>{{ minutes }}</abbr>",
scope: {
minutes: "="
}
};
return directive;
}
In some places, like on an ng-repeat, this works fine. But in other places, where my page controller is fetching data from the server, it's not.
The issue seems to be that the link function is firing when the page is being parsed, so when the data comes back from the server, the link function is not 're-run', so the tooltip is never set because the first time it ran the minutes was empty.
When it's in an ng-repeat, then the link function is only called when the data is already there, so it's fine.
How do I re-run the link function, or what is a better pattern for this directive to follow?
Lets say you have <custom obj="object"></custom>. Link function wont be called again if you modify object. What you can do:
Best - rewrite directive, so it watches object ($scope.$watch('obj') in directive)and if it changes directive updates itself. (This is how ng-bind, ng-repeat, ... works) Or you can use events.
If you receive object only once - just use ng-if, i.e. if object is promise:
<custom obj="object" ng-if="object.$resolved"></custom>
Pathetic fake ng-repeat:
<div ng-repeat="a in fakeArray">
<custom obj="object"></custom>
</div>
Here assigning fakeArray to ['anything'] will remove old element and add new.

How to update Directive on State Changes

I have a root state that defines the overall structure of the Angular template. In the root state, I have the sidebar included that has dynamic menus via directive that changes based on the state. Like this:
.state(‘root', {
abstract: true,
url: ‘/root',
templateUrl: ‘views/root.html',
})
root.html includes the sidebar.html that has dynamic menu called through Directive like this:
sidebar.html
<ul class="nav" id="side-menu">
<li class="nav-header">
<img alt="avatar" ng-src="{{ avatar }}" />
</li>
<!—Dynamic Menus Directive -->
<li sidebar-menus></li>
</ul>
The directive shows the menu based on $state.includes(). But what happens is, the directive shows fine in the first load but it doesn’t update the directive during state changes. To resolve this, I tried the following methods but nothing worked:
Added the $state to scope in Main controller but it still doesn’t change the directive
once it is compiled first.
Tried adding $stateChangeSuccess watcher to trigger recompiling the directive, but it doesn’t
recompile again after the first time (or) maybe it is recompiling but the changes are not seen in the template (this is the code I have now
which I will give below).
Moving the sidebar inside separate child
states instead of having it in root state works, but it beats the
purpose since I am trying to load the overall structure in the root
state first and only refresh the menu sections in subsequent state
changes.
I am not really sure how to approach this. I have a feeling my approach can be out of whack and hoping someone can guide me here. This is my directive code at the moment:
.directive('sidebarMenus', ['$compile', '$state', '$rootScope',
function($compile, $state, $rootScope) {
return {
restrict: 'A',
replace: true,
link: function(scope, element, attrs) {
var state = scope.$state; // Scope from Main Controller
// HTML Template
function contructHtml(state) {
var htmlText = '';
// First Child State
if (state.includes('root.child1')) {
var htmlText = '<li>Child 1 Menu</li>';
}
// Second Child State
if (state.includes('root.child2')) {
var htmlText = '<li>Child 2 Menu</li>';
}
// Third Child State
if (state.includes('root.child3')) {
var htmlText = '<li>Child 3 Menu</li>';
}
$compile(htmlText)(scope, function( _element, _scope) {
element.replaceWith(_element);
});
}
$rootScope.$on('$stateChangeSuccess', function() {
var state = scope.$state; // scope.$state is added in main controller
contructHtml(state);
});
// Initial Load
contructHtml(state);
}
}
}])
You can get rid of the compile business by using template.
You template could look something like this:
<li ng-if="state.includes('root.child1')">Child 1 Menu</li>
<li ng-if="state.includes('root.child2')">Child 2 Menu</li>
<li ng-if="state.includes('root.child3')">Child 3 Menu</li>
So your directive code should look sth like this
return {
restrict: 'A',
replace: true,
template:'<div> <li ng-if="state.includes('root.child1')">Child 1 Menu</li>
<li ng-if="state.includes('root.child2')">Child 2 Menu</li>
<li ng-if="state.includes('root.child3')">Child 3 Menu</li>
</div>'
link: function(scope, element, attrs) {
$scope.state = scope.$state; // Scope from Main Controller
$rootScope.$on('$stateChangeSuccess', function() {
$scope.state = scope.$state; // scope.$state is added in main controller
});
}
}

Adding a CSS class to element on ng-click

I have several arbitrary number of menu items on my page. Simplified they look like this.
...
I would like to add a particular class to an item that is being clicked so that its state changes compared to others.
I know I should do it sugin this kind of a pattern:
<a href=""
class="menu-item"
ng-class="{ active: clicked }"
ng-click="clicked = true">
...</a>
but the problem is that I can't use a single variable as I have an arbitrary number of items. I should either use X number of variables or use an array. But in any case how would I know which item goes with each variable/array index unless I manually enter those indices by hand?
What I'm missing
I'm missing element reference that I could use in ng-click so I could add a particular class on element itself. But that would somewhat bind $scope and UI even though I wouldn't be using any $scope function that would manipulate UI. I'd do it all in the view...
How am I supposed to do this?
A directive that solves this problem:
app.directive("markable", function() {
return {
scope: {}, // create isolated scope, so as not to touch the parent
template: "<a href='#' ng-click='mark()' ng-class='{ active: marked }'><span ng-transclude></span></a>",
replace: true,
transclude: true,
link: function(scope, elem, attrs) {
scope.marked = false;
scope.mark = function() {
scope.marked = true;
};
}
};
});
Use it as:
<a markable>Mark me</a>
Relevant fiddle: http://jsfiddle.net/9HP99/
The advantage of a directive is that it is very easy to use.
The same with a DOM-manupulating directive, through Angular's jQuery-ish interface:
app.directive("markable", function() {
return {
link: function(scope, elem, attrs) {
elem.on("click", function() {
elem.addClass("active");
});
}
};
});
Usage:
<a href="#" markable>Mark me</a>
Fiddle: http://jsfiddle.net/atE4A/1/
You could do something like this:
...
then you can use this.ClassName to get nth item.
But if you # of items is arbitrary, you may consider trying to format the list in a way where you could use ng-repeat, then it would be as easy as this:
...

How to update attribute on element via directive using angularjs

I have a pretty simple case in AngularJS where:
<select ng-repeat="el in elms" disabled="disabled" remove-disable>
<option>make a selection</option>
</select>
Initially my select is empty and so I added the disable attr to avoid having people click on it.
When the ajax call is completed and the select renders the list of options I want to remove the disable attribute.
It looks straight forward, right? but all I have seen is approaches using $watch and not for exactly this case.
I'm approaching it from a jQuery point of view where an looking at the DOM after the ajax call, finding the element and removing the attr. like this:
$('select').removeAttr('disabled');
Unfortunately I don't want to do jQuery, I want to do it with a directive, since that is what is for. the angular folks say that all DOM manipulations should be done via directives so I will like to know just how.
enrollmentModule.directive('removeDisable', function () {
return {
restrict: 'A',
scope: {
ngModel : '='
},
link: function (scope, element, attrs) {
console.log('no people yet');
if (element[0].complete) {
console.log('element finish rendering');
};
scope.$watch(attrs.ngModel, function () {
console.log('agents arrived');
});
}
};
});
AngularJS has a ngDisabled directive that you can use to make the link between the state of the list and an expression :
<select ng-repeat="el in elms" ng-disabled="elms.length == 0">
<option>make a selection</option>
</select>

Resources