angularjs $compile service and ng-repeat - javascript

Basically what I'm trying to do is build a directive that would take array as an isolate scope object. Build html iterating through the array with ng-repeat and compiling using $compile service against the directive's scope which would then be pushed into the content attribute of the popover. It works fine when ng-repeat is applied to the immediate parent node of the references. Fails when not. Can someone enlighten why it wouldnt work. Thanks in advance
Plunkr url: http://plnkr.co/edit/i5DlOWgHbyC8YovgKvt6?p=info
HTML
<a working data-names="['cat','dog','mouse']">Click to get a basic popover - working</a>
<br/>
<a not-working data-names="['cat','dog','mouse']">Click and you will get nothing</a>
JAVASCRIPT
app.controller('MainCtrl', function($scope) {
}).directive("working", function($log,$compile,$http){
return {
restrict: "A",
scope:{
names:'='
},
link: function(scope, elem, attrs){
$log.log(scope.names);//Logs Names
var html = "<p><a ng-repeat='name in names'>This is a {{name}}</a></p>";
var popOverContent = $compile(html)(scope);
$log.log(popOverContent);//Logs p.ngscope properly
var options = {
content: popOverContent,
placement: "top",
html: true
};
$(elem).popover(options);
}
};
}).directive("notWorking", function($log,$compile,$http){
return {
restrict: "A",
scope:{
names:'='
},
link: function(scope, elem, attrs){
$log.log(scope.names);//Logs Names
var html = "<p ng-repeat='name in names'><a>This is a {{name}}</a></p>";
var popOverContent = $compile(html)(scope);//Logs only a comment
var options = {
content: popOverContent,
placement: "top",
html: true
};
$(elem).popover(options);
}
};
});

apprently has to do with the popover structure not the scopes since both directives keep their ngrepeat in the same scope level, it appears that the popover needs to have only one root element in its template in the second example you are building several root elements
the only update was tha instead of
var html = "<p ng-repeat='name in names'><a>This is a {{name}}</a></p>";
i used
var html = "<div><p ng-repeat='name in names'><a>This is a {{name}}</a></p></div>";
http://plnkr.co/edit/eMGQFykGjEImzXFA0ffh?p=preview

Related

angular directive: switch between two templates dynamically

I am trying to create a directive named availableTo that can switch between two different templates depending on some message. For example, if the field is an input with the ng-model directive, I would first need to change it to read-only using the <span> tag. So far, my code can switch the view to read-only, but I cannot seem to switch it back to input:
var directive = {
restrict: 'A',
require: '?ngModel',
link: linkerFn,
replace: true
};
function linkerFn(scope, element, attrs, ngModelCtrl) {
var clonedElement = angular.copy(element);
var preOuterHTML = clonedElement[0].outerHTML; //this can save the <input> field html code
scope.$on('mode_changed', function() {
var curUserRole = userservices.getUserRole();
if (attrs.availableTo == curUserRole) {
var e = $compile(preOuterHTML)(scope);
element.replaceWith(e);
} else {
var template = '<span>' + ngModelCtrl.$viewValue + '</span>';
var e = $compile(template)(scope);
element.replaceWith(e);
}
}); //scope.$on
} //linkerFn
For an input field:
<input name="test1" class="form-control" ng-model="name" placeholder="Name 1" available-to="ADMIN"/>
I also noticed that once I change the template in the else block above, the element re-renders, and the preOuterHTML does not contain the original element html any more. This seems to be mission impossible to me, but I would like to hear some expert opinions. Thanks
element.replaceWith(e); Don't do that. In Angular, if you find yourself attempting to modify the DOM directly, you are by definition doing it wrong. You gotta sit back and let Angular do the work.
If you need to replace a directive's entire template, a fairly straightforward approach is to use ng-include with a scope variable containing the desired conditional templateUrl, e.g.
var directive = {
// ...
template: '<div ng-include="myTemplateUrl"></div>',
link: function(scope, el) {
if (/* whatever */) {
scope.myTemplateUrl="templates/foo.html";
} else {
//...etc
}
},
};
(This does add an extra DOM node to the tree, but that's generally harmless.)
It sounds like in your case you may not need to go that far, though; a simple ng-if inside your template is probably enough to swap between your read-only <span> and <input>.

how to idividually append directive to div in angularjs

HTML:
<div ng-app = "myApp" ng-controller = "someController as Ctrl">
<div class = "clickme" ng-repeat = "elems in Ctrl.elem" ng-click ="click()">
{{elems.title}}
click me
<div id="container">
</div>
</div>
angularjs
var Elems = [{title : "1"},
{title : "2"},
{title : "3"}];
var myApp = angular.module('myApp', []);
myApp.controller('someController', function($scope) {
var self = this;
self.elem = Elems;
$scope.click = function(){
//append directive <test-Input></test-Input>
};
});
myApp.directive('testInput',function(){
return {
restrict: 'E',
replace: false,
transclude: false,
template: '<div>asdasdasdasd</div>',
controller: function($scope) {
}
};
});
how to append directive to individual div when it clicked? I know it will be very easy with Jquery but it's hard to do in angular way.
You do not want to do it this way, one of Angulars advantages is that you do not have to deal with changing the HTML yourself, but use data-binding and templates instead, establishing an MVC-pattern. You are still thinking in jQuery ;-)
Change only your data on click and use ng-repeat to make angular append your template for you.
P.S.: Why do you use $scope in your controller? You are using controller as-sntax (which is good), you can simply bind the click()-funciton to the controller instead of $scope
Try this..
var modalElementTemplate = angular.element(Your template);
var linkFn = $compile(modalElementTemplate);
var modalElement = linkFn($scope);
$("#WheretoAppend").append(modalElement);
Instead of doing a huge DOM alteration put the testInput directive in the ngRepeat loop and add a check at the top of the directive whether the div container has been clicked or not (need scope isolation).

Access template child elements in angular directive

I'm trying make a directive out of a great jquery zooming/panning library. How can I access the elements in my template to initialize the plugin?
The directive looks like this as of now:
directive('zui', [function () {
return {
restrict: 'E',
scope: { URL: "#"},
template: '<div id="zui" ><div id="viewport" ><img ng-src="{{imageURL}}"></div></div>',
link: function (scope, element, attrs) {
scope.imageURL = URL;
var zui = new ZUI53.Viewport( document.getElementById('zui') );
zui.addSurface( new ZUI53.Surfaces.CSS( document.getElementById('viewport') ) );
var pan_tool = new ZUI53.Tools.Pan(zui);
zui.toolset.add( pan_tool );
pan_tool.attach()
}
};
}]);
Clearly document.getByID() is not the best way to accomplish this. What is a better solution? Thanks a lot.
Getting to the element using document.getElementById('zui') is indeed not nice. If you ever had to have two instances of the zui directive, it would break.
Don't use id on the root element. Use a marker class
such as class="zui" or a data attribute such as data-zui.
Using jquery, you can get to your element using find like
this: element.find('.zui')[0];.

Problems animating ng-repeat directive template with d3

I'm trying to make a category list with Angular and D3. I've created a directive for this list and some of the child nodes in my template use ng-repeat because I'd like to have angular build my html instead of d3.
My problem is that when trying to reference the ng-repeated elements with d3, they haven't been created yet. If I use the link function there are no items in the list. If I use the compile function I'm able to see 1 list item, but there should be at least 2.
Seen Here
Taken from fiddle:
angular.module('myApp').directive('targetingCategories', function(){
return {
restrict: 'E',
scope: {
data: '='
},
template: '<div>'+
'<ul class="catList">'+
'<li ng-repeat="cat in data.categories">'+
'{{cat.name}}'+
'<ul class="subCatList">'+
'<li ng-repeat="subcat in cat.categories">{{subcat.name}}</li>'+
'</ul>'+
'</li>'+
'</ul>'+
'</div>',
replace: true,
compile: function($tEl, $attrs){
// attach d3 on the TEMPLATE element, and look for list items
var vis = d3.select($tEl[0]);
var catList = vis.select('.catList');
var catListFirstItem = vis.select('.catlist>li');
var catListItems = vis.selectAll('.catList>li');
console.log(catList); // 1 item returned (the ul)
console.log(catListFirstItem); // 1 item returned (the first li)
console.log(catListItems); // 1 items returned ??
return function($scope, $el, $attrs){
// attach d3 to INSTANCE element and look for list items
var vis = d3.select($el[0]);
var catList = vis.select('.catList');
var catListFirstItem = vis.select('.catlist>li');
var catListItems = vis.selectAll('.catList>li');
console.log(catList); // 1 item returned (the ul)
console.log(catListFirstItem); // 1 item returned (the first li)
console.log(catListItems); // 0 items returned ??
};
}
};
});
The idea is to have angular build my markup, and d3 animate it. I like the control that d3 gives me versus using CSS animations.
When I used d3 to create my list items this was working just fine. I'm tempted to do that again, but I think putting the LIs in the template is a better architecture.
I'm thinking that I need to call a compile, or wait for one or something. I'm almost at the limit of my Angular knowledge so any education here is appreciated.
I'm adding my own potential answer, but leaving unmarked incase anyone else has something better.
For now, it seems that one way to accomplish this is to replace the LIs in my main template with other nested directives, specifically "mainCategory" and "subCat". I am then able to attach d3 to the appropriate instance element for animation. By including an index attribute property, I can control the delay to make animations staggered. It feels a bit convoluted and "backbone-esque" (objects for the sake of objects), but it solves my problems. It also allows me to attach some ng-clicks which would have been next to cumbersome/impossible to do with d3 creating my LIs.
Yes, I'm aware that my attribute is called "data" and could be problematic, but I'm not worried about that now. Any corrections on my use of services/controllers/directives are appreciated though :)
updated JSFiddle
angular.module('myApp').directive('targetingCategories', function(){
return {
restrict: 'E',
scope: {
data: '='
},
template: '<div>'+
'<ul class="catList">'+
'<main-category ng-repeat="cat in data.categories" index="$index">'+
'{{cat.name}}'+
'<ul class="subCatList">'+
'<sub-cat ng-repeat="subcat in cat.categories" index="$index" data="subcat">{{subcat.name}}</sub-cat>'+
'</ul>'+
'</main-category>'+
'</ul>'+
'</div>',
replace: true
};
});
angular.module('myApp').directive('mainCategory', function(targetingService){
return {
restrict: "E",
scope: {
index: "=",
data: "="
},
template: '<li ng-transclude ng-click="selectCategory($event, data);"></li>',
replace: true,
transclude: true,
link: function($scope, $el, $attrs){
d3.selectAll($el).transition()
.duration(500)
.delay(function(){ return $scope.index*200; })
.ease('elastic')
.style('width', '100%')
.style('padding', '10px');
$scope.selectCategory = function($event, cat){
$event.stopPropagation();
targetingService.selectCategory(cat);
};
}
};
});
angular.module('myApp').directive('subCat', function(targetingService){
return {
restrict: "E",
scope: {
index: '=',
data: '='
},
template: '<li ng-transclude ng-click="selectSubCategory($event, data)"></li>',
replace: true,
transclude: true,
link: function($scope, $el, $attrs){
d3.selectAll($el).transition()
.duration(100)
.delay(function(d,i){ return 500+($scope.index*200); })
.ease('linear')
.style('height', '30px')
.style('padding', '8px');
$scope.selectSubCategory = function($event, subCat){
$event.stopPropagation();
targetingService.selectSubCategory(subCat);
};
}
};
});

Angularjs/Jquery data()

How to attach arbitrary data to an html element declaratively, and retrieve it.
Please see the code. http://plnkr.co/edit/sePv7Y?p=preview
Angular has the jQuery data() support.
So, I want to attach data to each li element (say _data = node ) in the template, and later on to retrieve it using
var li = elm[0]....
console.log('li-', li.data('_data'))
li - {id:1}
Code:
'use strict';
var app = angular.module('Directives', []);
app.controller('MainCtrl', function ($scope) {
$scope.data = [
{id:1}, {id:2}, {id:3}
];
});
app.directive('test', function ($timeout) {
return {
template: '<li class="ch" ng-repeat="node in data">' +
'<span class="span2">' + 'id - {{node.id}}' + '</span>' +
'</li>',
restrict: 'A',
link: function (scope, elm, attrs) {
console.log(elm[0].children);
}
};
});
Edit:
Updated the code with how I like to set data.
template: '<li class="ch" ng-repeat="node in data" data-node="node">' +
couldn't select the li element properly now to see whether it is working
tried,
elm[0].children[0].data()
elm.children[0].data()
etc..
First of all, if it were some third party lib that you are trying to integrate with angular, that might be ok, but now you're generating DOM with angular and embedding data in the DOM. This is very strange.
Second, your test directive template uses ngRepeat, which creates isolate scope and you won't be able to access li items declaratively. You will have to use DOM traversal, which is also not very angular-way-ish.
Third, your view should be bound to model by angulars two-way bindings. Do not try to simulate opposite behaviour on top of that. Either you should not use angular or you should change your approach to your problem, because it will be pain to develop and maintain otherwise.
I would provide a real answer if you could describe what are you trying to achieve and why exactly do you need that model in data. Now the easiest solution would be ditching test directive and rewriting it as such:
controller's template:
<ul>
<li ng-repeat="node in data" model-in-data="node">
<span class="span2">id - {{node.id}}</span>
</li>
</ul>
directive modelInData
.directive('modelInData', function($parse) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
var model = $parse($attrs.modelInData)($scope);
$attrs.$set('data', model);
}
}
});
Here each li element adds it's model to the data attribute.

Categories