Template directive calling another directive - javascript

I am developing a multi data app that is using angular.js and d3.js. I am having a hard time to include the <svg> into my scope.
What is happening now is that the directive ngTests is being loaded before the ngRepeat executes it's methods.
I am putting the fiddle here so you guys can have a better idea.
jsfiddle
<ng-chart></ng-chart>
PS: I can get the td id on my ngTests directive, but it doesn't update at all <td ng-tests id="histogram{{$index}}".
If I change this line to <td ng-tests id="histogram">, use histogram as ID on my directive and change my ngTests directive to read only "#histogram" it creates my svg 6 times on the first table of my ngRepeat, which is not the result I am expecting.
Thank you.

Do two things. In your ngChart directive you have the index attribute specified like index="index" but it needs to be index="{{$index}}". Then, to read the attribute value from the other directive, instead of accessing the attribute directly like attrs.index you should be using attrs.$observe more like the following:
attrs.$observe('index', function(observedIndex) {
console.log('observedIndex:', observedIndex);
});
This way when the index is changed (e.g. by another directive) you get notified and can update the element's text. I forked the fiddle and got it working here.

Related

Get an element in DOM hierarchy from the link function of a directive [AngularJS]

I have a simple DOM hierarchy and I want to grab a specific set of elements (I want all the canvas elements). This is the entire template for this directive:
<div id='charts-container'>
<div class='chart-wrapper' ng-repeat='chart in getNumberOfCharts() track by $index'>
<canvas id="{{'exam-chart-' + $index}}" class='chart-canvas'></canvas>
</div>
</div>
I want to create a list with all the canvas within the #charts-container element, but I just can't. This is what I'm trying to do inside the link function:
link: function (scope, element, attributes, controller) {
var look = element.find('#chart-canvas');
$log.debug(look);
}
And then I get this element, but I don't know how to get all the chart-wrapper elements from here.
I tried doing look.context.children, it returned an empty list, but at the same time showed me what I wanted as if the list were populated, here is an image. If I try to access any index of this list, it returns undefined (which is fine, because it's an empty list. But why the console is showing me these values?)
What is the best way to achieve this? To get all the canvas elements in this template? (there are 15 of them). Thanks!
--- UPDATE ---
I realized that if I remove the ng-repeat attribute I'm using in the template, it works! But I need the ng-repeat...
element.find('.chart-canvas') manage to get the canvas element, but only if there are no ng-repeat attribute.
Is this a common issue when using ng-repeat. Is there any special treatment when falling in this case?
I found a solution in this thread.
Apparently I was trying to access DOM that hasn't been rendered yet. So, wrapping my query for the canvas with
$timeout(function() {
var canvasList = element.find('.chart-canvas');
}, 0);
solved the issue, since using $timeout will wait until all $digest cycles are complete.
I would like to get to know better how the rendering pipeline works in AngularJS, to avoid falling in issues of this kind. If anyone has a link to a thread or explanation of this, it would be much appreciated.
I think it should be element.find('.chart-canvas') instead of element.find('#chart-canvas'). Does it work with that change ?

AngularJS directive wait until templates rendered

I'm trying to wrap the SharePoint People Picker in an AngularJS directive. In order to initialise a people picker I need to place a div on the page, give it an ID and pass that ID into a SharePoint function.
I have this working with a basic directive like this:
<sp-people-picker id="test"></sp-people-picker>
But I wish for the directive to be useable anywhere, including in a repeating section:
<div ng-repeat="item in dataset">
<sp-people-picker id="test-{{ $index }}"></sp-people-picker>
</div>
This fails. I stepped through the code to see what was going wrong and found that while I was happily calling the SharePoint people picker function with "test-0" it was failing to find the element. document.getElementById("test-0") returned null. The reason for this is that my div still had the id "test-{{ $index }}" and only gets "test-0" AFTER my directive has compiled.
How can I make sure my directive runs after the {{ }} has been rendered?
(Not tagging with SharePoint as the SharePoint stuff is just the context, it's not actually relevant to the issue I'm trying to solve)
You need to use attrs.$observe inside your directive link function, that will act as the same as like $watch, the difference is it can watch on the {{}} interpolation directive, Your link function will look like below. It call function whenever interpolation directive gets evaluated.
Directive(Link Function)
link: function(scope, element, attrs){
attrs.$observe(attrs.id, function(newVal, oldVal){
//here you can get new value & `{{}}` is evaluated.
});
}

Angular custom directive - two way binding which always sets attribute to true or false

I'm creating a custom Angular directive for a slide in menu which needs to watch a couple of attributes and one of those attributes needs to be two way bound to the main controller scope (sometimes). However, sometimes the attribute will not be added by the developer so it needs to be added automatically and set to the default (false). So, the directive can be used like this.
<slide-menu position="right" is-open="menuIsOpen"></slide-menu>
or like this:
<slide-menu></slide-menu>
When used the first way the main controller will be able to open and close the menu by changing the value of the boolean $scope.menuIsOpen.
When used without supplying the is-open attribute it should default to false and is obviously used internally and by a child toggle directive.
An additional complication is that whether the attribute is supplied by the developer or not it should exist in the DOM. so in the second example above the directive would set itself to false by default and add the attribute is-open="false" to the DOM?
The reason for requiring is-open="false/true" in the DOM at all times is that the menu is actually operated using CSS tansitions which use the following selector:
slide-menu[is-active="true"]{
// Slide the menu in using transforms/transitions
}
There is a jsfiddle here which shows how far I have got.
http://jsfiddle.net/jonhobbs/gEPvE/
Obviously it doesn't work, but it shows how I have tried to set a default and how I have tried to use # and & on the isolated scope for a one time binding (the menu position) and a 2 way bound expression for the is-open variable.
I'm clearly a long way from achieving what I need but any advice would really be appreciated.
Have a look at this fiddle http://jsfiddle.net/gEPvE/38/
I took the one you started and updated it to act like you specified.
You can make a two way binding value optional by adding a ? on the scope definition.
Like this
{
scope: {
'isOpen':'=?'
}
}
Now the is-open attribute is optional.
Then you can set the default value in the directive controller, like you had started to do.
Next, in order to synchronize the DOM attribute with the scope value you can use $watch.
$scope.$watch('isOpen', function(val) {
$element.attr('is-open', val);
});
Finally, I changed the second 'slideMenuToggle' directive to wrap/transclude its element in order to add an ng-click handler. This is mainly to avoid any nastiness with calling $scope.$apply yourself.
Let me know if that works for you.
EDIT
Answering your question in the comment, you can pass a value directly without having it be bound to the scope, you just need to wrap the value in quotes.
For example
<div ng-controller='ctrl'>
<hello world='imOnScope'></hello>
</div>
Assuming 'hello' is a directive with a scope of 'world': '=?' then angular will assign a reference to the parent scope's 'imOnScope' object to the directive's $scope.world member, allowing a two way binding scenario.
To just provide a value directly you may do something like this
<div ng-controller="ctrl">
<hello world="'directValue'"></hello>
</div>
In this scenario angular will just assign 'directValue' to the directive's $scope.world member.
You need to add ngTouch to your module.
var app = angular.module('app', ['ngTouch']);
And add this script:
http://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-touch.js
The reason for requiring is-open="false/true" in the DOM at all times
is that the menu is actually operated using CSS tansitions which use
the following selector
Forcing directive attributes to be appropriate for css selectors is terrible idea. As you correctly stated, they are for developers. So add a class to the element dynamically.
It seems that you're misusing &, it would be ok to set up a callback, but since you don't do this, in its current state you can end up with one-way # with confidence.
I guess it can be something like this (just added ngTouch and ng-controller for parent scope).
You could replace
$scope.watch('isOpen', function () {
$element.toggleClass('opened', $scope.isOpen);
});
with
$scope.watch('isOpen', function () {
$attrs.isOpen = !!$scope.isOpen;
});
and get the behaviour you're asking for, easy as that. Ok, it is boolean now, and it reflects the scope, and you can use [is-open=true] selector. But guess what will happen with your binding? Broken. Fortunately, you can do
$scope.watch('isOpen', function () {
$element.attr('is-open', !!$scope.isOpen);
});
instead. Voila, we tricked Angular because it doesn't look after jqlite. But what will will happen with the binding when the directive will be re-compiled for any reason? Again, isOpen's binding is non-existing 'true' or 'false' scope variable. Broken.

What is the best way to only display some of the html elements in the ng-repeat loop

I want to use an ng-if inside an ng-repeat (as described in this question):
However, I want to display a row in a table if and only if the if condition holds. In the answers to the question above, you will get empty div tags where the if condition does not hold. This does not cause much of a problem with divs, but when doing the same with tables, you do not want to have div (or span) tags inbetween tr tags.
Is there another tag or directive that I could use?
A better idea is to use a (custom) filter.
That way, only the needed rows are generated, in stead of hiding/removing existing ones.
I just made a plunk for another question, that showed how to use a filter:
the relevant line is this one:
<tr ng-repeat='item in appVm.users |filter:test'>
you can use a object directly in your code like this:
<tr ng-repeat='item in appVm.users |filter:{age:32}'>
If you put that line in the sample I linked in, you can see how that would work out!
Does this help you?
If you just want to hide these elements, you can consider using ng-show instead of ng-if.
Maybe you can also add the ng-show in the ng-if tag to hide it when empty.
Now, if you do not want to render the content at all, I think the only solution is to remove these elements from your collection in your controller
you can use ng-show.its works for show and hide tags.if you don't want to angular compile inside the tag you can use ng-if.

Getting an Element in AngularJS

It seems that getting an element in AngularJS is a bad idea, i.e. doing something like:
$('.myElement')
in say, a controller is not an angular way of doing things.
Now my question is, how should I get something in angular?
Right now, what I'm doing (and is an accepted way of doing it) is by watching a variable, and my directive does something based on it.
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.id == varToWatch)
{
//Run my Directive specific code
}
});
However, while this particular design works for most cases, watch is an expensive operation, and having lots of directives watching can really slow down your application.
TL:DR - What is an angular way of getting a directive based on a variable on the directive? (like the one above)?
If you want to get/set values you don't need to fetch the element using jQuery. Angular data binding is the way to do it.
directives is the way to go if you want to do animations or any kind of element attributes and DOM manipulation.
Your code is basically right; the directive should watch something in the $scope and perform it's logic when that thing changes. Yes, watch statements are expensive, and that is a problem once your number of watches start to approach ~2000.
Looking at your code though, I see one problem:
The variable $scope.varToWatch references an id in the template.
When this variable changes, you want something to happen to the element which has this id.
The problem here is in the first point: The controller should know nothing about the DOM, including the id of any element. You should find another way to handle this, for example:
<div my-directive="one"> ... </div>
<div my-directive="two"> ... </div>
<div my-directive="three"> ... </div>
...etc
And in your directive:
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.myDirective == varToWatch)
{
// Run my Directive specific code
}
});
You are very vague as to what you're trying to achieve, but I'll try to answer in context of your last comment.
I have a lot of the same directives (therefore the code will run on all of them), but I need to get only one directive from the lot.
You talk a lot about getting the right element. The directive element is passed to the link function in the directive. If you are not using this element (or children of it) directly, but rather trying to search for the element you want somehow, you are most likely approaching the problem the wrong way.
There are several ways to solve this, I'm sure. If you're thinking about animations, there is already support for that in Angular, so please don't try reinvent the wheel yourself. For other logic, here are two suggestions:
Secondary directive
If the logic you want to apply to this directive is generic, i.e. it could be applied to other directives in your application, you could create a new directive which works together with directives. You can set prioritization in directive in order to control which directive is executed first.
<main-directive ... helper-directive="{{condition_for_applying_logic}}"></main-directive>
jsFiddle example
Expanding main directive
If the logic is tightly coupled to this directive, you can just create a new attribute, either dynamic or static, and bind to it in the directive. Instead of checking 'attrs.id == varToWatch', you check if $scope.apply-logic === 'true' and apply the logic then.
<main-directive ...></main-directive> <!-- Not applied here -->
<main-directive apply-logic="true" ...></main-directive> <!-- Applied here -->
<main-directive apply-logic="{{some.varOnScope}}"...></main-directive> <!-- Conditional -->
Please comment if something is unclear.

Categories

Resources