How do I do dynamic template manipulation in an AngularJS directive? - angularjs

I'm just getting started on Angular and am trying to wrap my head around proper directive use. I'm writing a custom directive that takes an object array and parses it into a variable number of vertical divs. It's basically a grid system where the elements are arranged into stacked vertical columns rather than in rows. The number of divs dynamically varies with the width of the screen, requiring dynamic changes in the div class as well as reconstructing the ordering of the array elements in the div columns as the page resizes.
When I use the contents of the template as plain, static HTML, everything loads just fine. The filters dynamically change the dataset when you use the input fields, etc.
When I use my directive, the initial page-load looks fine. However, dynamic filtering is broken - it is no longer bound to the input fields. More importantly, on a page resize, the HTML fails to compile at all, leaving a blank screen and uncompiled directive tags in the DOM.
I don't know Angular well enough to troubleshoot this. If I had to guess, it sounds like something is not being bound properly on the page $compile due to a problem with scope.
Note: I know doing string concat for the template is poor practice but I just want to get things working before I start messing around with nesting directives.
Edit: here's a link to the Github repo for my front-end code: https://github.com/danheidel/education-video.net/tree/master/site
HTML
<body ng-controller="channelListController">
Creator: <input ng-model="query.creators">
Tags: <input ng-model="query.tags">
query: {{query}}
<div id="channel-view">
<channel-drawers channels="channels"></channel-drawers>
</div>
</body>
JS
.controller('channelListController', function ($scope, $http){
$http.get('api/v1/channels').success(function(data){
$scope.channels = data;
});
})
.directive('channelDrawers', function($window, $compile){
return{
restrict: 'E',
replace: true,
scope: {
channels: '='
},
controller: 'channelListController',
//templateUrl: 'drawer.html',
link: function(scope, element, attr){
scope.breakpoints = [
{width: 0, columns: 1},
{width: 510, columns: 2},
{width: 850, columns: 3},
{width: 1190, columns: 4},
{width: 1530, columns: 5}
];
angular.element($window).bind('resize', setWindowSize);
setWindowSize(); //call on init
function setWindowSize(){
scope.windowSize = $window.innerWidth;
console.log(scope.windowSize);
_.forEach(scope.breakpoints, function(point){
if(point.width <= scope.windowSize){
scope.columns = point.columns;
}
});
var tempHtml = '';
for(var rep=0;rep<scope.columns;rep++){
tempHtml +=
'<div class="cabinet' + scope.columns + '">' +
'<div class="drawer" ng-class="{' + ((rep%2 === 0)?'even: $even, odd: $odd':'even: $odd, odd:$even') + '}" ng-repeat="channel in channels | looseCreatorComparator : query.creators | looseTagComparator : query.tags | modulusFilter : ' + scope.columns + ' : ' + rep + '">' +
'<ng-include src="\'drawer.html\'"></ng-include>' +
'</div>' +
'</div>';
}
console.log(tempHtml);
element.html(tempHtml);
$compile(element.contents())(scope);
}
}
};
})

The $compile function should be implemented when you want to manipulate your template. The link function should be implemented when you want to bind your template to your scope and/or setup any watchers. If you have dynamic HTML that you're inserting into your DOM, then ask your self these questions:
Are you modifying the template? If so, then create an element template (angular.element(...)) and append it to your element parameter.
Have you modified the template (step 1) and your template contains binding expressions, interpolation expressions, and/or attributes that should bind from other templates? If so, you need to compile and link your element you created from step 1.
Here is an example:
.directive('myDirective',function($compile) {
restrict: 'E',
scope: '=',
compile: function(element, attr) {
// manipulating template?
var e = angular.element('<div ng-model="person">{{person.name}}</div>');
element.append(e);
// the following is your linking function
return function(scope, element, attr) {
// template contains binding expressions? Yes
$compile(e)(scope);
};
}
});
To fix your code, try moving the template manipulation to your compile function, and in your linking function, call $compile(e)(scope).

First, thanks to pixelbits and pfooti for their input. It put me on the right track. However, I wanted the answer to be a clean slate since our discussions got into technical matters that ended up being tangential to the actual answer.
Basically, this question is poorly framed. After doing more reading, it became clear that I was using the Angular elements in ways they aren't really intended for.
In this case, I'm doing a bunch of model manipulation in my directive and it really should occur in the controller instead. I ended up doing that and also moving the window resize handler code to another component.
Now, I don't even need a directive to properly format my data. A couple of nested ng-repeats with a dusting of ng-class and ng-style do the job just fine in 2 lines of HTML.
<div ng-repeat="modColumn in splitChannels"
ng-class="{'col-even' : !$even, 'col-odd' : !$odd}"
ng-style="{ width: 99 / windowAttr.columns + '%' }"
class="cabinet">
<div ng-repeat="channel in modColumn"
ng-class="{'row-even' : !$even, 'row-odd' : !$odd}"
ng-cloak
class="panel roundnborder">
<ng-include src="'panel.html'"></ng-include>
</div>
</div>
If I could give a bit of advice from one Angular beginner to another, it would be this: if your code is getting complex or you're digging into the internals of things, step back and rethink how you're using the Angular components. You're probably doing an action that should be done in another component class. Proper Angular code tends to be very terse, modular and simple.

Is there a reason you're doing it with templates?
Can't you bind the number of columns to a variable on the scope? I've done similar stuff with just fiddling around with either ng-if directives to hide stuff that's not important now, or to have general layout stuff attached to the current scope (I generally stuff it all into properties on $scope.view)
There's also plenty of this kind of stuff that already works in css3's media selectors as well, without needing mess with the DOM at all. Without a clearer picture of what you're trying to accomplish I'm not sure if this is super-necessary. More than one ways to skin a cat, etc etc.
Otherwise, #pixelbits is right - if you are fiddling with the DOM tree directly, that needs to happen in compile - values going into the DOM goes into link.

Related

Using Two-Way Binding with Custom Directive and ng-repeat

Starting this off by saying that I know a common answer for this is to put the ng-repeat inside the content of the directive, but in this case I can't figure out how that will work for this.
Basic Problem
As the charCounter increases throughout the letter spans, I need to access the offsetTop value of each span in the ng-repeat in order to do some things, (When the charCoutner gets to a new line (has offsetTop > 0), adjust the begin variable that is in the limitTo in the ng-repeat).
Struggling Point
I am not able to update a variable in the custom directive and make it accessible to the ng-repeat.
If I have the custom directive outside of the ng-repeat I have no access to the offsetTop of each span (but the begin variable updates).
<p><span shell counter="charCounter" begin="begin"><span ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></span></p>
What I'm thinking that needs to happen is this, but how do I get the begin variable to be updated?
<p><span positioner counter="charCounter" begin="begin" ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></p>
Code Pen Sample
I've been playing around with this on Code Pen, You can see this problem in code pen here:
UPDATE
I've taken the example one step further and also integrated r0m4n's feedback about upping the priority of this and #Amy Blankenship's about further clarifying what I am trying to do. Here is the updated CodePen. While this technically works, I'm not thinking I'd even need to do a custom directive now, since I'm accessing the element manually rather than using the element from the directive. I still have a delicate understanding of all this.
Your scope binding is having some priority issues. Take a look at using the priority property. Per the docs:
"When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied"
So you could do something like this:
myApp.directive('positioner', function($timeout) {
return {
restrict: 'A',
priority: 1001,
scope: {
letter: '=',
begin: '=',
counter: '='
},
link: function($scope, element, attr) {
$timeout(function() {
$scope.begin += 5;
console.info('begin', $scope.begin);
}, 1000);
}
};
});
https://docs.angularjs.org/api/ng/service/$compile

variable templateURL in angular

I've only been working with angular for 2 weeks so fairly new to the framework.
I'm making an app that shows data through charts, and i want the data to be viewable in different chart types. The idea is that you can click a button and swap the chart type.
The way i've been doing this is by rendering the chart through a directive using templateURL. Unfortunatly i've been unable to make the templateURL variable. I've tried different things and this is how it looks atm:
main.html:
<chart charttype="{{chartType}}"></chart>
directive:
.directive("chart", function () {
return {
restrict: 'E',
templateUrl: function (element, attrs) {
return 'views/partials/' + attrs.charttype + '.html';
}
}
Controller:
$scope.chartType = "lineChart";
$scope.ChangeChart = function (chartType) {
$scope.chartType = chartType;
}
The code is supposed to render 3 different html files (lineChart.html, barChart.html & pieChart.html). however the {{chartType}} is simply parsed as a string
It works when i use:
<chart charttype="lineChart"></chart>
For whatever reason i can't get the charttype attribute to become variable.
I also realize this might be more of a rails way of fixing an angular problem (i'm used to rendering partials). So maybe this is not the way to do this in angular. I've thought about other ways to do this like hide/show the charts but this just seems ugly to me. I'm up for any suggestions though.
update 1:
so i'm trying to render it via ng-include in all the ways i can think of but it keeps giving me errors or doesn't show anything at all.
i've also tried putting the ng-include directly into my html file
<div ng-include="views/partials/lineChart.html"></div>
However in my browser i see it just comments this line out.
update 2:
I couldn't get ng-include to work with a variable template. So i've decided to solve the problem by removing the templates and using ng-hide in my html file instead.
Once the template url is set, it is not called again. Use ng-include with variable template
Something like
template: "<div ng-include=\"'views/partials/' + charttype + '.html'\"></div>"
Directive templateUrl parameter can't get variable value as argument, just static text. if you want, i can show solution with ng-include directive in directive template parameter.

attribute-directive used with ng-attr or ng-class

preface
I've seen many questions regarding using ng-attr and directives however I have yet to see this specific case being implemented.
code & plunkr
http://bit.ly/1s6gWkD
use case
I'm trying to dynamically add a loading overlay into target DOM elements via an attribute directive. The idea is that by virtue of the target DOM element possessing the attribute directive, the DOM will have the overlay appended to its children.
I've approached this from various angles with no luck. Because this is going to be used in many places where we might want to block certain UIs but not fully block the app with a modal, I am hoping to keep our templates clean and attach this dynamically.
questions
is this possible (assuming there IS a directive life-cycle event to tackle this), BTW this is what I would call the dynamic approach
if not possible, I did try a few less-than-ideal 'static' approaches using this such as with no luck
ng-class="{loadOverlay: hasOverlay}"
ng-attr-load-overlay="hasOverlay"
observations
I do realize that there may be an issue with this approach as once the attribute is removed, there may not be a life-cycle event in the directive to know that it is ordered to remove itself. I don't know enough about directives to know if this is the case.
ideally what I'm looking for
target DOM element w/o overlay
target DOM element w/ overlay
settling for the static approach
After giving this some thought I think having a more versatile directive combined with the 'static' approach is the best.
plunkr solution
http://bit.ly/1toMCV9
snips
.directive('loadOverlay', function() {
return {
restrict: 'EA',
scope: true,
link: function(scope, element, attrs) {
var id = 'nx-load-overlay-' + parseInt(Math.random() * 1000);
function toggleOverlay(show) {
if (show === true) {
var d = '<div id="' + id + '" class="nx-load-overlay"><div class="nx-load-overlay-spinner"><span class="fa fa-cog fa-spin fa-3x"></span><br/><span style="font-weight:bold; font-size:larger;">loading</span></div></div>';
element.append(d)
} else {
$('#' + id).remove()
}
}
if (attrs.loadOverlay)
scope.$watch(attrs.loadOverlay, toggleOverlay);
else
toggleOverlay(true)
}
}
})

AngularJS - ngRepeat inside a custom directive's transclusion is not working as expected

I am having troubles implementing a custom directive with transclude: true that can have a "transclusion" content that is using ngRepeat.
[My case]
I want to implement a set of directives, that are fetching the data that they are supposed to show from a $http service. For that I want to use preLink phase interceptor that Angular provides, so I can catch the data and set it to the scope. That way if I have some dynamic (since this term is well overloaded - I mean a data which structure is unknown until the request is done) data coming from the service, I rely on that, that I will be able to retrieve a list with that dynamic data and store it inside the scope, then loop through that data via ngRepeat inside the HTML. Here comes my problem...
[My Problem]
Angular is not using the list that I am assigning to the scope during preLink.
[A plunkr]
I maded a plunker that illustrated just the problem that I am having.
http://plnkr.co/edit/XQOm4KWgKxRhn3pOWqzy?p=preview
[My question]
I really believe that such functionality is covered by angular and I am just missing something in the puzzle.
Can anyone tell me how to implement such behaviour?
Thanks!
EDIT:
Thank you rchawdry for your answer. Here are some details on my intentions. To make it simple I will try to give you an example.
Let's assume that we have these directives:
1. "page" - This directive is a labeled container for all the page content. Visually it is represented as some div - for header, for content and for other fancy stuff if needed. The directive does not know what is its data before the page loads. As the page loads the directive must retrieve the information for itself and its children from a REST resourse! Then the directive is setting the information needed for itself (label and other stuff) and stores its children content in childrenList scope variable. It creates a scope.
2. "section" - This section can be child of "page". Since "page" is retrieving its data from a server, then the information about how many "section"s does our "page" have is dynamic and we don't know how many "section"s we need to show on the screen. This depends on sectionList that is coming from the back-end. The section itself is almost the same as "page" - it is a labeled container, with the differences that - a). "section" is container of elements; b). "section" does retrieve its data from its parrent instead of making $http request. This directive creates a scope.
3. "element" - For this example, in order not to define many different elements and complicate it, let's assume that I have one element, called "element". It can consist of some "input" with "span" and "button" if needed. It is similar to the "section" with that, that it retrieves the data to show from it's parrent (in the general case, this is "section" or "page"). On the other hand it is different than "section" by the fact that it has no transcluded content.
Now after we have some of the concept here is what I am trying to achieve:
<page>
<element id='element1' someOtherStuffHere...></element>
<section id='static_section1' someOtherStuffHere...>
<element id='element2' someOtherStuffHere...></element>
</section>
<div class="row" ng-repeat="section in sections">
<section dynamic_id='dynamic_section'>
<div class="row" ng-repeat="elem in elements">
<element dynamic_id='dynamic_element'></element>
</div>
</section>
</div>
</page>
well, I believe that what your trying to achieve will be able by adding a ng-repeat attribute to the transcluded template.
by letting angular know about the 'repeat', it is supposed to work.
since plunkr is currently unavaliable, I can't prodivde any preview and do not have your original code. Ill try to recall it:
template: "<div id='container'>" +
"<div class='content' ng-repeat='item in [1]' ng-transclude'></div>" +
"</div>"
edit: http://plnkr.co/edit/xba4pU666OGxBtKtcDwl?p=preview
You've got a scope problem. The controller is using a variable that isn't defined in the controller (arrayListItemsPre and arrayListItemsPost). While they are declared in the directives, accessing them in a transcluded scope is a little tricky.
The easy way is to do the following. This will present the scope variables up to the controller where they can be used.
app.directive('container', function($compile) {
return {
transclude: true,
scope: true,
replace: true,
restrict: 'E',
template: "<div class='container'>" +
"<div class='content' ng-transclude></div>" +
"</div>",
compile: function(cElem, cAttrs) {
return {
pre : function preLink(scope, iElement, iAttrs) {
console.log("Hello from Container preLinkPhase");
scope.$parent.arrayListItemsPre = [1, 2];
},
post : function postLink(scope, iElement, iAttrs) {
scope.$parent.arrayListItemsPost = [1, 2];
}
};
}
};
});
There are other ways to do this that are better but it requires understanding why you're trying to iterate on variables that are defined in the directive. If you're going to be using this directive in another page that has different array elements, you'd have to change the directive code.

AngularJS and nesting Directives

I want to create a directive that roughly represents a table. Also I want to create a directive that defines the columns in that table. The table directive ng-repeats on the tr and displays the custom columns per row.
I'm pretty new to angular. Any help would be appreciated, here is the code and jsfiddle link.
As you can see from the fiddle this doesn't work yet.
http://jsfiddle.net/P6Nq4/
HTML:
<div ng-app='myApp' ng-controller='myController'>
Table of People:
<my-table>
<my-td my-data-name='Name'></my-td>
<my-td my-data-name='Description'></my-td>
</my-table>
<button>done</button>
</div>
Javascript:
angular.module('myApp', [])
.controller('myController',function($scope){
$scope.items = [{Name: 'John', Description: 'Awesome'},
{Name: 'Pat', Description: 'Ambiguous'}];
})
.directive('myTable', function(){
return {
link: function(scope,element,attrs){
},
transclude:true,
template: '<table><tr ng-repeat="item in items" ng-transclude>'
+'<td>static column at the end</td></tr></table>'
};
}).directive('myTd', function(){
return {
link: function (scope,element,attrs){
scope.data = attrs.myDataName;
},
template: '<td><input ngModel="eval(\'item.\' + data)" /></td>'
}});
A few things you need to change:
restrict property of the directive definition object. By default, directives are restricted to being html attributes - if you want to use an element (like you have), you need to set restrict to include an E. Link to docs.
You had ngModel="eval(\'item.\' + data)". First issue with this is that in html, angular uses a hyphenated form, and not camelCase, so change ngModel to ng-model. Secondly, the ng-model attribute is automatically evaluated against the scope, so you don't need to use eval. The better approach is to use javascript's bracket notation to access the property. So your ng-model binding becomes:
Finally, if you are re-using a directive, you almost always want to create a new scope, if not an isolate scope, otherwise scopes can interfere with eachother. In your case, the data from the second column overwrites the first column. To fix this, force the creation of a new scope with scope: true
You can see all of the changes in the updated fiddle. I would suggest commenting out the scope line and making sure you understand what happens if you don't have separate scopes, as it's a little bit subtle.
EDIT
Regarding your static column, I think it makes more sense (and it's much simpler) if you include your static column as static html. So instead of putting it in your my-table template, you add it like so:
<my-table>
<my-td my-data-name='Name'></my-td>
<my-td my-data-name='Description'></my-td>
<td>static column</td>
</my-table>
I think this is simpler, and also keeps the html semantic, and representative of what it will be compiled into. This shows it working

Resources