I am trying to include my d3 code inside a directive.
However, when my directive is inside a ng-repeat, the transitions won't take place.
Here's a JSFiddle of the issue: http://jsfiddle.net/hLtweg8L/1/ : You can see that when you click on the button, the rectangles position doesn't change smoothly, and 'append' is logged to the console once again.
My directive is the following:
myMod.directive('chart',function(){
return {
restrict:'A',
scope:{
data:'=',
},
link:function(scope,elem,attrs){
a=d3.select(elem[0]);
rects=a.selectAll("rect").data(scope.data,function(d));
rects.enter().append("rect")
.attr("x",function(d,i){console.log('append');return i*50+"px"})
.attr("y",100)
.attr("width",35)
.attr("height",function(d){return d.age*10+"px"})
.attr("fill","blue")
rects.transition().duration(200)
.attr("x",function(d,i){console.log('append');return i*50+"px"})
.attr("y",100)
.attr("width",35)
.attr("height",function(d){return d.age*10+"px"})
.attr("fill","blue")
}
}
})
As far as I understand it, the problem is that the elem passed inside the link function is not the same when the ng-repeat gets updated, that's why the append gets called more than once for the same data.
My question is: How can I use d3 transitions inside ng-repeat ? (Corrected Jsfiddle would help a lot). Or why is the elem not the same between different calls ? Can I tell angular that the dom shouldn't be removed and added again ?
A couple things are needed:
If you don't want ng-repeat to create a new element, you need to use the track by option so that it knows how to identify new vs. changed items:
<div ng-repeat="set in sets track by set.group">
D3 will not automatically see that the data has changed unless your directive watches for changes.
a=d3.select(elem[0]);
scope.$watch('data', function() {
updateGraph();
});
Here is an an alternate Fiddle:
http://jsfiddle.net/63tze4Lv/1/
Related
Essentially is there a simple way to leverage the ng-enter and ng-leave hooks on the ng-repeat directive to animate a basic slider left and right?
You can see the plunk here which is almost working.
I have a basic slider that transitions through a list of elements one page at a time using a partition filter and ng-repeat. This works as expected except when you switch directions, in which case the ng-leave transition on the previous direction is run causing a erroneous result.
I could write a custom slider directive or even use the angular-ui carousel but I don't want to over-complicate such a simple example if indeed a simple solution does exist. Any help would be much appreciated.
This screenshot shows the problem, and points to the solution.
The element passed to the animation leave function has the old class value.
Oddly, I have not been able to replicate this screenshot because the $scope mysteriously became unavailable. $rootScope is always available though. But it does seem you could do this without injecting $rootScope.
My solution is this:
Decorate the $animate.leave function like this:
.config(function($provide) {
$provide.decorator("$animate", function ($delegate, $rootScope) {
$delegate.originalLeave = $delegate.leave;
$delegate.leave = function (element, doneCallback) {
// THIS IS THE ADDITIONAL FUNCTIONALITY
$(element).removeClass("right").removeClass("left").addClass($rootScope.direction);
$delegate.originalLeave(element, doneCallback);
};
return $delegate;
});
})
Then place the direction variable on the $rootScope, which will of course need to be injected into the controller.
This solution requires jQuery, but it could be easily done without it.
Here is the working Plunker
Use ng-fx! Documentation and install instruction can be found here
Here is an example of how it can be used with ng-repeat :
<ul ng-controller="FoodController">
<li class='fx-fade-down fx-speed-800 fx-easing-bounce' ng-repeat="food in foods">
{{ food }}
</li>
</ul>
This will use a fx-fade-down effect on the ng-repeated item, in 800 ms, with an 'easing' of bounce.
Additionally "Note that ng-repeat will not trigger animations upon page load, the collection you are iterating over must be empty at first then populated, you can achieve this with a simple timeout or some other async operation" :
angular.module('foodApp', ['ngAnimate', 'fx.animations'])
.controller('FoodController', function($scope, $timeout){
$timeout(function(){
$scope.foods = ['apple', 'muffin', 'chips'];
}, 100);
});
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.
I'm trying to implement a Angular UI Bootstrap carousel, but I'm using it for a Quiz. Therefore, I don't need normal Prev() and Next() buttons.
Rather, I need a custom Next() button that makes sure they've selected an answer before continuing on to next "slide" of question/answers.
How do I hook into the carousel directive functions to run my code and then use the carousel.next() function?
Thanks,
Scott
There is no official possibility to achieve this. but this can be hacked, if you want. But i think it is better grab the bootstrap original one, have a look the at angular bootstrap ui sources (carousel) and write your own wrapper.
Here comes the hack:
The first problem we have to solve is, how to access the CarouselController. There is no API that exposes this and the carousel directive creates an isolated scope. To get access to this scope wie need the element that represents the carousel after the directive has been instantiated by angular. To achieve this we may use a directive like this one, that must be put at the same element as our ng-controller:
app.directive('carouselControllerProvider', function($timeout){
return {
link:function(scope, elem, attr){
$timeout(function(){
var carousel = elem.find('div')[1];
var carouselCtrl = angular.element(carousel).isolateScope();
var origNext = carouselCtrl.next;
carouselCtrl.next = function(){
if(elem.scope().interceptNext()){
origNext();
}
};
});
}
};
});
We must wrap our code in a $timeout call to wait until angular has created the isolated scope (this is our first hack - if we don't want this, we had to place our directive under the carousel. but this is not possible, because the content will be replaced). The next step is to find the element for the carousel after the replacement. By using the function isolateScope we have access to the isolated Scope - e.g. to the CarouselController.
The next hack is, we must replace the original next function of the CarouselController with our implementation. But to call the original function later we have to keep this function for later use. Now we can replace the next function. In this case we call the function interceptNext of our own controller. We may access this function through the scope of the element that represents our controller. If the interceptNext returns true we call the original next function of the carousel. For sure you can expose the complete original next function to our controller - but for demonstration purposes this is sufficient. And we define our interceptNext function like this:
$scope.intercept = false;
$scope.interceptNext = function(){
console.log('intercept next');
return !$scope.intercept;
}
We can now control the next function of the carousel by a checkbox, that is bound to $scope.intercept. A PLUNKR demonstrates this.
I knew this is not exactly what you want, but how you can do this is demonstrated.
That hack is neat michael, I started working on something similar for my needs. But then realized I might as well finally dip my toe into contributing to the open source community.
I just submitted a pull request to update the library so the index of the current slide is exposed to the Carousel scope.
https://github.com/angular-ui/bootstrap/pull/2089
This change allows you to have per-slide behavior in the carousel template.
This change allowed me to override the base carousel template so that for instance on the first slide the "prev" button would not show or the "next" button would not show for the final slide.
You can add more complex logic for your own personal needs, but exposing the current index in this manner to the $scope is part of making this part of the framework more flexible.
EDIT
I made more changes for my personal use, but don't want quite yet to contribute this change which is closer to what you are needing.
I modified the carousel directive, adding the "finish" property to scope.
.directive('carousel', [function () {
return {
restrict: 'EA',
transclude: true,
replace: true,
controller: 'CarouselController',
require: 'carousel',
templateUrl: 'template/carousel/carousel.html',
scope: {
interval: '=',
noTransition: '=',
noPause: '=',
finish: '='
}
};
}])
Then, when I declare the carousel, I can pass in a method to that directive attribute which is a method in the scope of the controller containing the carousel.
<carousel interval="-1" finish="onFinish">
...
</carousel>
This allows me to modify my template to have a button that looks like this:
<button ng-hide="slides().length-1 != currentIndex" ng-click="finish()" class="btn next-btn">finish<span class="glyphicon glyphicon-stats"></span></button>
So it only shows conditionally on the correct slide and with ng-click it is calling the carousel's $scope.finish() which is a pointer to a method in the controller I created for this application.
Make sense?
edit: This only works if you don't use sort functionality with ng-repeat. There is a bug which breaks the indexing of the slides for this kind of functionality.
I am having a sortable list that gets populated by data in my Angular Controller. This list also contains an extra element containing some controls that can also be dragged
What I want do do, is make sure that the extra element stays in place after the $digest cycle is run, and I made a directive just for that case.
App.directive('ngIgnore', ['$timeout',function($timeout){
return {
link: function(scope, element){
scope.$watch(function(){
// Keep track of the original position of the element...
var el = element[0];
var siblings = Array.prototype.slice.call(el.parentNode.children);
var parent = el.parentNode;
var index = siblings.indexOf(el);
$timeout(function(){
// After the digest is complete, place it to it's previous position if it exists
// Otherwise angular places it to it's original position
var item;
if(index in parent.children)
item = parent.children[index];
if(!!item){
parent.insertBefore(el, item);
}
});
});
}
}
}]);
It worked, but not as I wanted it to... As you can see in this example shuffling the list does not move the ignored element, but the problem is that the $watch function gets executed infinitely since the $timeout triggers another $digest cycle... I tried changing the $timeout with setTimeout but no luck...
Is there any Angulary way to run code right after $digest is executed? Or is my entire logic wrong here?
Another (highly recommended) solution is to stop thinking in DOM manipulations.
Common problem with developers who start writing AngularJS code is tend to do DOM manipulations as the result of some event. Its common in jQuery:
$('#someid').click(function() { this.toggleClass('clicked') })
In AngularJS you should design your View to visualize your Model state (that should be in $scope). So
<div ng-click="clicked = !clicked" ng-class="{clicked: clicked}">I was clicked</div>
Same logic should be applied when designing components. In a HTML code you should put all visual logic - hide some elements using ng-show/ng-if, ng-switch. Add/remove classes using ng-class etc. So you define all possible model states.
Then by just changing model state you will get your view automatically updated reflecting current model state.
Same goes for repeated elements: you can repeat on some collection and then, depending on what element is present, you define how it would look. Keep in mind, that in ng-repeat each element will have own child (so mostly independed) Scope, so you can define some per-element manipulations.
See also this great answer: "Thinking in AngularJS" if I have a jQuery background?
You can try to use Scope.evalAsync Scope.evalAsync:
it will execute after the function that scheduled the evaluation
(preferably before DOM rendering).