I have a directive that is repeated using ng-repeat. Consider this code:
<my-drctv todos="todos" ng-repeat="todos in todoGroups track by todos[0].id"></my-drctv>
todoGroups is a two dimensional array, that contains grouped todos.
This is my directive:
angular.module('myModule').directive('myDrctv', function() {
return {
restrict: 'E',
scope: {
todos: "="
},
replace: true,
templateUrl: '<div>{{firstTodo.description}}</div>',
link: function link(scope, element, attrs, ctrl) {
console.log("myDrctv: link fn");
scope.firstTodo = scope.todos[0];
}
};
});
This lists my todos (the template is simplified) and works so far.
Now the problem: When todoGroups is re-fetched from my server and set to a new value with $scope.todoGroups = myService.fetch() inside my controller, my directives aren't updated, because the link function of my directive isn't called a second time.
It seems that this problem occurs only, when the length of the list returned by the server is the same as before and only the "content" of my todos are changed (but I'm not sure)
Does angular re-use my directives for performance reasons? I always thought, it would destroy every element inside ng-repeat and insert new elements (in my case a directive)
How can I trigger a second call to my link function of my directive?
Related
I would like to use a standard input control that is decorated with ng-model and ng-required and then add my own custom attribute directive that provides uib-typeahead functionality to the control.
I used this link to get my directive partly working.
Add directives from directive in AngularJS
PLUNKR - The Version 2 of the directive does not work correctly with ng-model
My Directive does add typeahead functionality and that works quite well, but it is not binding the model on to the control after item is selected.
I have two version of my directive.
Version 1: is an element style directive and I have been using it successfully for a while, but it fell short when I wan't to have a bit more control over the input element, especially when I wanted to use ng-required='true' and other ng-message directives.
Version 2: is an attribute style directive, I went with this because I felt it was better to just add the typeahead functionality that I wanted to any standard HTML that can optionally use ng-required='true', ng-model etc...
While this directive is mostly working, it does not interact correctly with ng-model and I'm not sure how to get it working
angular.module(APP)
.directive('wkLocationSuggest', ['$compile', function ($compile) {
return {
restrict: 'A',
require: 'ngModel',
replace: false,
//terminal: true,
//priority: 0,
scope: {
wkApiModel: '=' // Provide access to the internal data that is returned via the API lookup
},
controller: 'LocationSuggestController',
link: function (scope, element, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
element.attr('typeahead', 'location as row.location for row in typeAhead($viewValue)');
element.attr('typeahead-wait-ms', '750');
element.attr('typeahead-on-select', 'onSelectInternal($item, $model, $label)');
element.attr('typeahead-min-length', '2');
element.attr('typeahead-focus-first', 'true');
element.removeAttr("wk-location-suggest"); //remove the location-suggest to avoid indefinite loop
element.removeAttr("data-wk-location-suggest"); //also remove the same attribute with data- prefix if it exists
// None of this is working
//// invoked when model changes from the outside
//ngModelCtrl.$render = function () {
// //scope.innerModel = ngModelCtrl.$modelValue;
//};
////// invoked when model changes from the inside
//scope.onChange = function (value) {
// ngModelCtrl.$setViewValue(scope.innerModel);
//};
scope.onSelectInternal = function ($item, $model, $label) {
// This fires, but it effects the ng-model on the first input,
// but not the input that this directive is attached too
ngModelCtrl.$setViewValue($item.location);
};
$compile(element)(scope);
}
};
}]);
These two images demonstrate part of the problem, may be better to test for yourself using PLUNKR above
I initially tried to dynamically add validators to your wk-location-suggest-new directive by implementing blur on the input element in combination with ngModel's $setValidity method; but don't know what exactly was preventing the event from firing.
Therefore, I turned to the other directive wk-location-suggest-old and tweaked it a bit to fit in both desired behaviors.
There, I noticed that you were missing a couple of things:
First of all, in order for a form element to glue with the form itself (wkProfileCompany in your case), and to work with ng-model, the element (in the directive template) needs a name.
Secondly, ng-required (or required) would work with the form only if it is added as an attribute to the element in the directive template, not the directive which compiles to the template containing the element.
Directive Definition
As you may notice, I've passed two properties from the outer scope to the directive's inner scope, namely:
the name of the input element,
and an isRequired flag as to specify whether the input is required or not.
.
.directive('wkLocationSuggestOld', [function () {
return {
restrict: 'E',
require: '?ngModel',
scope: {
name: '#', // <==
isRequired: '=' // <==
},
template: '<input name="{{name}}" type="text" class="{{innerClass}}" ng-model="innerModel"'
+ ' ng-change="onChange()" uib-typeahead="location as row.location for row in typeAhead($viewValue)" '
+ ' typeahead-wait-ms="750" typeahead-on-select="onSelectInternal($item, $model, $label)" '
+ ' typeahead-min-length="2" typeahead-focus-first="true" '
+ ' ng-required="isRequired">', // <== added ng-required here
controller: 'LocationSuggestController',
link: function (scope, element, attrs, ngModel) {
if (!ngModel) {
return;
}
...
}])
HTML
Finally, you can use the tweaked directive in your HTML as such:
<wk-location-suggest-old class="form-control" type="text" name="location2" ng-model="location2" is-required="true"></wk-location-suggest-old>
Plunker
Update
One of the possible reasons for ng-model not correctly binding in the wk-location-suggest-new directive to a provided value (i.e. location3) is that you are replacing the whole DOM element with a new custom DOM element which is compiled with the isolated scope of the directive itself.
Since the directive wk-location-suggest-new has an isolate scope, the scope is totally unaware of location3, because location3 (and all the other location values) are defined in the scope of MainCtrl and NOT the scope of the directive itself; therefore, you'll end up binding the input's value to an undefined property.
link: function (scope, element, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
...
$compile(element)(scope); // <== here
You need to update your model in setTimout() like below as you have an isolated scope in the directive.
setTimeout(function () {
scope.$apply(function () {
scope.location3 = 'Your selected value'
});
}, 2000);
Alternatively you can also utilize $timeout service to achieve the same result.
For my particular application, I want to be able to use an ng-repeat on a custom directive and track by a property of the object.
The problem I'm running into is that I am intentionally not wanting my custom directive to be rendered in the DOM where it is written. I want to take the template (including the directive element) and some variables that exist in it's parent scope and have them compiled and appended inside some markup that is being generated by a 3rd party library.
This way I can write something like:
<my-directive ng-repeat="item in items track by item.id"
ng-click="someCtrl.doSomethingWithAService();">
</my-directive>
And have this code appended elsewhere where it functions the same way.
The collection that is being repeated over can change frequently, but I don't want to recreate the appended content, and want to be able to remove it when the scope of each directive instance is destroyed.
I've tried as many approaches as I can think of, and some have been "good enough". The main issue I'm running into now is that when the collection is updated, the tracked items that didn't change end up being added to the DOM where the markup was originally written.
So on first load, everything works. Update collection: directive logic doesn't run again and the element is appended back to where the ng-repeat was.
Any ideas?
Edit:
window.angular.module('my.module')
.directive('myDirective', ['myService',
function (myService) {
return {
restrict: 'E',
scope: true,
replace: true,
templateUrl: '/path/to/template.tpl.html',
compile: function (tElement, tAttributes) {
tElement.removeAttr('ng-repeat');
return {
post: function (scope, iElement, iAttributes, controller, transcludeFn) {
iElement.remove();
var item = myService.addItem(scope, iElement[0].outerHTML);
scope.$on('$destroy', function () {
// remove item added in service
});
}
};
}
};
}]);
The directive essentially looks like this.
Service:
window.angular.module('my.module')
.factory('myService', ['$compile', function ($compile) {
function addItem(scope, template) {
var element = angular.element(document.querySelector('some-selector'));
element.html('').append($compile(template)(scope));
}
return {
addItem: addItem
};
}]);
I am developing a widget where I want to render some messages/text one after another. I want to change the template of the message based on the type of message.
my current directive setup is as follows
directive('cusMsgText', function(){
return {
restrict: 'E',
template:function(elements, attrs){
return '<div></div>';
},
link: function($scope, iElm, iAttrs, controller) {
//add children to iElm based on msg values in $scope
}
};
});
The directive is used as follows
<div ng-repeat="(key, value) in chatUser.msg">
<data-cus-msg-text msg="value.type"></data-cus-msg-text>
</div>
Now my question are -:
Is it possible to return one of multiple strings (templates) from
template function itself based on the actual value of attribute
msg. I tried accessing attrs.msg in template function and it
return value.type.
If not then, Is it good to manipulate template under linker or I
need to move it to compile function?
To render a different template based on value.type you can use the ng-switch statement:
<div ng-switch="value.type">
<div ng-switch-when="type1">
//...template for type 1 here...
</div>
<div ng-switch-when="type2">
//...template for type 2 here...
</div>
</div>
Also, if I understood your second question: manipulation of the uncompiled directive should be done in the compile function, all the manipulation which occurs after compilation should go in the link function.
Docs for ngSwitch
EDIT: +1 to Sebastian for understanding what you wanted. However, what he is proposing is essentially reinventing the wheel, since it is essentially compiling and inserting the template manually (which is what ngSwitch does for you). Also, you can access the attributes you put on your directive through the attrs argument of the link function.
In the template function you don't have access to the scope of your directive. If you want to control what gets rendered you can do this using conditional logic (e.g. ng-switch) in a global template as suggested by simoned or use a link function:
.directive('cusMsgText', function($compile) {
return {
restrict: 'E',
scope: {
msg: '=',
item: '='
},
link: function(scope, element, attrs) {
templates = {
x: '<div>template x {{item.name}}</div>',
y: '<div>template y {{item.name}}</div>'
};
var html = templates[scope.msg];
element.replaceWith($compile(html)(scope));
}
};
});
I want to maintain a list of directives on a page in the order they appear in the dom. I know directives are created (link function called) in order, and I can append them to an array when being linked, but how do I handle dynamic pages (ajax, ngRepeats, etc..). Currently every time I need to use the array I broadcast an event to get the directives in order.
gatherDirectives: ->
all = []
$rootScope.$broadcast 'roleCall', (dir) -> all.push dir
all
But I'd rather have directives register and unregister when being created and removed to be more efficient. Something like what is discussed on AngularJS directive - setting order for multiple directive elements (not priority for directives, but priority for the elements), but that can handle dynamically added/removed directives. How can this be done without gathering the directives each time?
"I want to maintain a list of directives on a page in the order they appear in the dom."
Can these methods help you?
Priority
AngularJS finds all directives associated with an element and processes it. This option tells angular to sort directives by priority so a directive having higher priority will be compiled/linked before others. The reason for having this option is that we can perform conditional check on the output of the previous directive compiled. In the below example, I want to add btn-primary class only if a div has btn class on it.
<div style='padding:100px;'>
<div primary btn>Random text</div>
</div>
Please note that the default priority if not set will be zero. In this example, btn directive will be executed before primary. Play with the demo!
App.directive('btn', function($timeout) {
return {
restrict: 'A',
priority: 1,
link: function(scope, element, attrs) {
element.addClass('btn');
}
};
});
App.directive('primary', function($http) {
return {
restrict: 'A',
priority: 0,
link: function(scope, element, attrs) {
if (element.hasClass('btn')) {
element.addClass('btn-primary');
}
}
};
});
Terminal
As per the official documentation, If set to true then the current priority will be the last set of directives which will execute on an element. It holds true unless you use custom directives in conjunction with built-in directives having priority set on them such as ngRepeat, ngSwitch, etc. Instead all custom directives having a priority greater than or equal the current priority will not be executed in this case.
In the below example, first has a higher priority than second – which has terminal set to true. And if you set the lower priority to first – It will not be executed at all. But in case of no-entry directive, it will not be executed even though it has a higher priority than ng-repeat. Is it a bug? Is it because of transclusion used in ng-repeat? Need to dig in…
<div first second></div>
<ul>
<li ng-repeat="item in ['one', 'two', 'three']" no-entry>{{item}} </li>
</ul>
App.directive('first', function() {
return {
restrict: 'A',
priority: 3,
link: function(scope, element, attrs) {
element.addClass('btn btn-success').append('First: Executed, ');
}
};
});
App.directive('second', function() {
return {
restrict: 'A',
priority: 2,
terminal: true,
link: function(scope, element, attrs) {
element.addClass('btn btn-success').append('Second: Executed ');
}
};
});
App.directive('noEntry', function() {
return {
restrict: 'A',
priority: 1001,
link: function(scope, element, attrs) {
element.append('No Entry: Executed ');
}
};
});
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.