I have same directive used on two different places in one .html file. I want to be able to use these two directives with different options. Here's the code that should make this requirement more clear:
<ul multisortable="{ stop: 'updateFirstColumn', receive: 'receiveFirstColumn' }"
class='connected'>
<li ng-repeat='task in tasks.firstcolumn> {{task.title}} </li>
</ul>
<ul multisortable="{ stop: 'updateSecondColumn', receive: 'receiveSecondColumn' }"
class='connected'>
<li ng-repeat='task in tasks.secondcolumn> {{task.title}} </li>
</ul>
I would like to create directive, which is able to recognize that, if it is called by element from first column, it should work with options defined for that element. So I should be able to distinguish it, and fetch updateFirstColumn, or updateSecondColumn, e.g, depending on whether it is called by moving items from first or second column.
In my directive, in link function I did something like this:
link: function postLink(scope, iElement, iAttrs) {
$timeout(function (){
$scope.$watch('tasks', function(newValue, oldValue) {
if (newValue){
angular.element($element).multisortable({
selectedClass: 'ui-selected',
connectWith: '.product-backlog-column'
})
}
}, true);
}, 0);
}
And it looks like my directive always uses only options that I pass from second element.
I can't get it to work with options from first, or from second element, depending on which one is being dragged.
Does anyone have an idea, why this is happening?
Maybe I should define separate scope within directive definition, or something like that?
I tried many things but with no luck.
Thanks.
UPDATE:
If I remove $watch and $timeout, options passed with directive work fine, but jQuery call that I'm using isn't working fine. I placed jquery plugin inside controller part in directive.
Your jquery plugin is probably doing something outside of angulars knowledge. You will have to do a $apply() to let angular know to update.
To read up on how the $apply() method works, you can find it in the docs: http://docs.angularjs.org/api/ng.$rootScope.Scope#$apply
Related
Here's a plunker example you can see: http://plnkr.co/edit/NQT8oUv9iunz2hD2pf8H
I have a directive that I would like to turn into a web component. I've thought of several ways as to how I can achieve that with AngularJS but am having difficulty with a piece of it. I'm hoping someone can explain my misstep rather than tell me a different way to do it.
Imagine you have a directive component that sets up some shell with css classes maybe some sub components, etc.. but lets the user define the main content of the component. Something like the following:
<my-list items="ctrl.stuff">
<div>List Item: {{ item.name }}</div>
</my-list>
The HTML for the list directive could be something like the following (with OOCSS):
<ul class="mas pam bas border--color-2">
<li ng-repeat="items in item track by item.id" ng-transclude></li>
</ul>
Normally this can be solved in the link function by linking the directives scope to the new content. And it does work for other components. However introducing the ng-repeat seems to break that portion of the control. From what I can tell, the appropriate place might be the compile function but the documentation says the transcludeFn parameter will be deprecated so I'm not sure how to proceed.
I should also note that when using the beta AngularJS, there is either a bug or a new paradigm coming, because this is no longer a problem. It seems like the transcluded content always gets access to the directives scope as well as the outer controllers scope.
I really appreciate any enlightenment on this.
It's by design that content added via ng-transclude will bind with an outer controller scope, not a scope of the current element that ng-transclude is on.
You could solve the problem by copy the ng-transclude's code and modify it a bit to give a correct scope:
.directive('myTransclude', function () {
return {
restrict: 'EAC',
link: function(scope, element, attrs, controllers, transcludeFn) {
transcludeFn(scope, function(nodes) {
element.empty();
element.append(nodes);
});
}
};
});
And replace the ng-transclude with my-transclude in your directive template.
Example Plunker: http://plnkr.co/edit/i7ohGeRiO3m5kfxOehC1?p=preview
Given this very simplistic markup:
<div ng-repeat="item in items" item>
And the directive:
app.directive('item', function()
{
return function(scope, element, attrs)
{
}
})
Say on a click event, I want to get the next item's scope and do something. I can either use scope.$$nextSibling() or element.next().scope().
Is there an advantage/disadvantage of using either? Or is there a better way of getting the next sibling's scope?
you should do like this
in the scope, call $rootScope.$broadcast('SOME_CHANGE_IN_SCOPE',...);
in sibling scope, call $scope.$on('SOME_CHANGE_IN_SCOPE',...)
you can of course wrap data in the event, check angularjs doc for more details
Not sure what you're trying to do, but you should never use any angular variables that begin with $$. They are meant for internal implementation and are not guaranteed to stay around from release to release.
I'm using angularjs and need to find the watch of the ng-repeat, because I need ng-repeat to stop working from a specific point. this my code:
<ul>
<li ng-repeat="item in items">
<div>{{item.name}}</div>
</li>
</ul>
I need to find the watcher of the ng-repeat. If I go to scope.$parent.$$watchers[x] and perform splice the watch is removed, but how can I find the specific watch?
It's not possible to find the watch (see explanation below), but you can achieve what you wish by use the following directive
app.directive('repeatStatic',
function factory() {
var directiveDefinitionObject = {
restrict : 'A',
scope : true, // this isolates the scope from the parent
priority : 1001, // ng-repeat has 1000
compile : function compile() {
return {
pre : function preLink(tElement, tAttrs) {
},
post : function postLink(scope, iElement, iAttrs, controller,
transcludeFn) {
scope.$apply(); // make the $watcher work at least once
scope.$$watchers = null; // remove the $watchers
},
};
}
};
return directiveDefinitionObject;
}
);
and its usage is
<ul>
<li repeat-static ng-repeat="item in items">
{{ item }}
</li>
</ul>
See http://plnkr.co/k9BTSk as a working example.
The rational behind is that
the angular directive ng-repeat directive uses internal function $watchCollection to add a self created listener that watchs the items object. Since the listener is a function created during the process, and is not keep anywhere as reference, there is no good way to correctly identify which function to remove from the $$watchers list.
However a new scope can be forced into the ng-repeat by using an attribute directive, in this way the $$watchers added by ng-repeat are isolated from the parent scope. Thus, we obtain full control of the scope.$$watchers. Immediate after the ng-repeat uses the function that fills the value, the $$watchers are safe to be removed.
This solution uses hassassin's idea of cleaning the $$watchers list.
I have a fork of Angular that lets you keep the watch in the $$watchers but skip it most of the time. Unlike writing a custom directive that compiles your HTML it lets you use normal Angular templates on the inside, the difference is that once the inside is fully rendered the watches will not get checked any more.
Don't use it unless you really genuinely need the extra performance because it can have surprising behaviour.
Here it is:
https://github.com/r3m0t/angular.js/tree/digest_limit
Here's the directive you can use with it (new-once):
https://gist.github.com/r3m0t/9271790
If you want the page to never update you can use new-once=1, if you want it to sometimes update you can use new-once=updateCount and then in your controller run $scope.updateCount++; to trigger an update.
More information: https://github.com/Pasvaz/bindonce/issues/42#issuecomment-36354087
The way that I have dealt with this in the past is that I created a custom directive that copies the logic of the built in ngRepeat directive but never sets up the watches. You can see an example of this here, which was created from the ngRepeat code from version 1.1.5.
The other way, as you mentioned was to remove it from $$watchers of a scope, which is a little stranger since it accesses a private variable.
How this could be done is that you create a custom directive on the repeat to remove the watch that is the repeat. I created a fiddle that does this. It basically just on the last element of the repeat clears the parent watch (which is the one on data)
if (scope.$last) {
// Parent should only be the ng-repeat parent with the main watch
scope.$parent.$$watchers = null;
}
This can be modified to fit your specific case.
Hope this helps!
It sounds like you want to put the bindonce directive on your ng-repeat.
https://github.com/Pasvaz/bindonce
If you don't need angular dual-binding, have you tried a custom directive with a complie function where you construct HTML yourself by creating DOM from scratch without any angular dual-binded mechanisms ?
I've been reading through various post, questions, answers and documentation but haven't managed to solve my problem so far.
I'm using mbExtruder jQuery plugin, and to integrate it within angular, I've created a directive for it:
directive('mbExtruder', function($compile) {
return {
restrict : 'A',
link : function(scope, element, attrs) {
var mbExtruderConfigurationAttrs = scope.$eval(attrs.mbExtruder);
mbExtruderConfigurationAttrs.onExtContentLoad = function() {
var templateElement = angular.element(angular.element(element.children()[0]).children()[0]);
var clonedElement = $compile(templateElement)(scope);
scope.$apply();
var contentElement = angular.element(angular.element(angular.element(element.children()[0]).children()[0]).children()[0]);
contentElement.replaceWith(clonedElement.html());
};
element.buildMbExtruder(mbExtruderConfigurationAttrs);
$.fn.changeLabel = function(text) {
$(this).find(".flapLabel").html(text);
$(this).find(".flapLabel").mbFlipText();
};
I'm extracting the container div, compiling it, applying scope and replacing the original div with the transformed one.
Now, I'm using the ability of mbExtruder to load contents from separate HTML file which looks like this:
<div>
<ul>
<li ng-repeat="property in properties">
<span><img ng-src="../app/img/{{property.attributes['leadimage']}}"/></span>
<a ng-click="openDetails(property)">{{property.attributtes['summary']}}</a>
</li>
<div ng-hide="properties.length">No data</div>
</ul>
</div>
And, in the HTML of the view I have following:
<div id="extruderRight"
mb-extruder="{position: 'right', width: 300, positionFixed: false, extruderOpacity: .8, textOrientation: 'tb'}"
class="{title:'Lista', url:'partials/listGrid.html'}">
</div>
The scope I'm getting in the directive is the scope of the parent controller which actually handles properties array.
The thing is that, if the properties list is pre-populated with some data, that ends up in the compiled HTML - cool. But, any change to properties does actually nothing. I've added watch handler on properties within directive and really, that is triggered whenever any change is made to properties, but, ng-repeat does not pick that up. The original idea is to have properties list empty in the beginning - that causes ng-repeat compile to have just this output:
<!-- ngRepeat: property in properties -->
Is this doing a problem? The fact that ng-repeat declaration has actually disappeared from DOM.
Am I missing something obvious here? I've read the documentation on $compile and ng-repeat and I would say that I don't need to manipulate DOM by myself, ng-repeat should do its work. Or I'm totally wrong?
Thanks.
EDIT: Plunker
You're passing in
contentElement.replaceWith(clonedElement.html());
Notice here that you're essentially replacing it with raw HTML string code. Hence, Angular has no concept of the directives inside the code. However, I don't see why this is needed at all. Just compiling and attaching the result to scope seems to work just fine:
var templateElement = angular.element(angular.element(element.children()[0]).children()[0]);
$compile(templateElement)(scope);
Here's the updated, working version:
http://plnkr.co/edit/gxPAP43sx3QxyFzBitd5?p=preview
The documentation for $compile didn't say too much, but $compile returns a function, which you then call with the scope you want to attach the element to (as far as I've understood). Hence, you don't need to store the reference to the element at all, unless you want to use it later.
Say I have an ng-repeat with a big array.
When ng-repeat runs, it adds every element of that array to an isolated scope, as well as having the array itself in a scope. That means that $digest checks the entire array for changes, and on top of that, it checks every individual element in that array for changes.
See this plunker as an example of what I'm talking about.
In my use case, I never change a single element of my array so I don't need to have them watched. I will only ever change the entire array, in which case ng-repeat would re-render the table in it's entirety. (If I'm wrong about this please let me know..)
In an array of (say) 1000 rows, that's 1000 more expressions that I don't need evaluated.
How can I deregister each element from the watcher while still watching the main array?
Perhaps instead of deregistering I could have more control of my $digest and somehow skip each individual row?
This specific case is actually an example of a more general issue. I know that $watch returns a 'deregisteration' function, but that doesn't help when a directive is registering the watches, which is most of the time.
To have a repeater with a large array that you don't watch to watch every item.
You'll need to create a custom directive that takes one argument, and expression to your array, then in the linking function you'd just watch that array, and you'd have the linking function programmatically refresh the HTML (rather than using an ng-repeat)
something like (psuedo-code):
app.directive('leanRepeat', function() {
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attr) {
scope.$watch('data', function(value) {
elem.empty(); //assuming jquery here.
angular.forEach(scope.data, function(d) {
//write it however you're going to write it out here.
elem.append('<div>' + d + '</div>');
});
});
}
};
});
... which seems like a pain in the butt.
Alternate hackish method
You might be able to loop through $scope.$$watchers and examine $scope.$$watchers[0].exp.exp to see if it matches the expression you'd like to remove, then remove it with a simple splice() call. The PITA here, is that things like Blah {{whatever}} Blah between tags will be the expression, and will even include carriage returns.
On the upside, you might be able to just loop through the $scope of your ng-repeat and just remove everything, then explicitly add the watch you want... I don't know.
Either way, it seems like a hack.
To remove a watcher made by $scope.$watch
You can unregister a $watch with the function returned by the $watch call:
For example, to have a $watch only fire once:
var unregister = $scope.$watch('whatever', function(){
alert('once!');
unregister();
});
You can, of course call the unregister function any time you want... that was just an example.
Conclusion: There isn't really a great way to do exactly what you're asking
But one thing to consider: Is it even worth worrying about? Furthermore is it truly a good idea to have thousands of records loaded into dozens of DOMElements each? Food for thought.
EDIT 2 (removed bad idea)
$watch returns a function that unbinds the $watch when called. So this is all you need for "watchOnce":
var unwatchValue = scope.$watch('value', function(newValue, oldValue) {
// Do your thing
unwatchValue();
});
Edit: see the other answer I posted.
I've gone and implemented blesh's idea in a seperable way. My ngOnce directive just destroys the child scope that ngRepeat creates on each item. This means the scope doesn't get reached from its parents' scope.$digest and the watchers are never executed.
Source and example on JSFiddle
The directive itself:
angular.module('transclude', [])
.directive('ngOnce', ['$timeout', function($timeout){
return {
restrict: 'EA',
priority: 500,
transclude: true,
template: '<div ng-transclude></div>',
compile: function (tElement, tAttrs, transclude) {
return function postLink(scope, iElement, iAttrs, controller) {
$timeout(scope.$destroy.bind(scope), 0);
}
}
};
}]);
Using it:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
Note ng-once doesn't create its own scope which means it can affect sibling elements. These all do the same thing:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
<li ng-repeat="item in contents">
<ng-once>
{{item.title}}: {{item.text}}
</ng-once>
</li>
<li ng-repeat="item in contents">
{{item.title}}: {{item.text}} <ng-once></ng-once>
</li>
Note this may be a bad idea
You can add the bindonce directive to your ng-repeat. You'll need to download it from https://github.com/pasvaz/bindonce.
edit: a few caveats:
If you're using {{}} interpolation in your template, you need to replace it with <span bo-text>.
If you're using ng- directives, you need to replace them with the right bo- directives.
Also, if you're putting bindonce and ng-repeat on the same element, you should try either moving the bindonce to a parent element (see https://github.com/Pasvaz/bindonce/issues/25#issuecomment-25457970 ) or adding track by to your ng-repeat.
If you are using angularjs 1.3 or above, you can use the single bind syntax as
<li ng-repeat="item in ::contents">{{item}}</li>
This will bind the value and will remove the watchers once the first digest cycle is run and the value changes from undefined to defined for the first time.
A very helpful BLOG on this.