AngularJS Directive using Compile cannot access child elements - angularjs

My intent was to create a directive that could rearrange (not reorder) its child elements into a Bootstrap CSS Grid, but I am having a lot of difficulty getting access to the child elements.
I've tried a lot of different things and have researched Compile vs Link vs Controller directive options. I think I might have to change the 'compile' to 'link' in my directive to get this to work, but I am unsure how to do that.
I have an AngularJS directive on GitHub that takes an array or object of parameters to render a simple or complex grid.
In the example below you can see the layoutOptions.data = [3, 4] which means the grid will have 3 cells in the top row and 4 in the second. This is working well.
The second step is that I would like to render some divs as child elements of the directive and the directive will place these in the cells of the grid as it is created. This is shown by the layoutOptions.content = ['apple', 'orange', 'pear', 'banana', 'lime', 'lemon', 'grape'] but this needs to be de-coupled so that it could be literally anything.
HTML Input
<div ng-app="blerg">
<div ng-controller="DemoCtrl">
<div class="container" hr-layout="layoutOptions">
<div ng-transclude ng-repeat="fruit in layoutOptions.content">{{fruit}}</div>
</div>
</div>
</div>
Desired (not actual) Output
Actual output is as below, but does not include the inner DIVs with fruit names
<div class="container hr-layout" hr-layout="layoutOptions">
<div class="row">
<div class="col-md-4"><!-- from ng-repeat --><div>apple</div></div>
<div class="col-md-4"><!-- from ng-repeat --><div>orange</div></div>
<div class="col-md-4"><!-- from ng-repeat --><div>pear</div></div>
</div>
<div class="row">
<div class="col-md-3"><!-- from ng-repeat --><div>banana</div></div>
<div class="col-md-3"><!-- from ng-repeat --><div>lime</div></div>
<div class="col-md-3"><!-- from ng-repeat --><div>lemon</div></div>
<div class="col-md-3"><!-- from ng-repeat --><div>grape</div></div>
</div>
</div>
And a jsFiddle that uses it here: http://jsfiddle.net/harryhobbes/jJDZv/show/
Code
angular.module('blerg', [])
.controller('DemoCtrl', function($scope, $timeout) {
$scope.layoutOptions = {
data: [3, 4],
content: ['apple', 'orange', 'pear', 'banana', 'lime', 'lemon', 'grape']
};
})
.directive("hrLayout", [
"$compile", "$q", "$parse", "$http", function ($compile, $q, $parse, $http) {
return {
restrict: "A",
transclude: true,
compile: function(scope, element, attrs) {
//var content = element.children();
return function(scope, element, attrs) {
var contentCount = 0;
var renderTemplate = function(value, content) {
if (typeof content === 'undefined' || content.length <= contentCount)
var cellContent = 'Test content(col-'+value+')';
else if (Object.prototype.toString.call(content) === '[object Array]')
var cellContent = content[contentCount];
else
var cellContent = content;
contentCount++;
return '<div class="col-md-'+value+'">'+cellContent+'</div>';
};
var renderLayout = function(values, content) {
var renderedHTML = '';
var rowCnt = 0;
var subWidth = 0;
angular.forEach(values, function(value) {
renderedHTML += '<div class="row">';
if(Object.prototype.toString.call(value) === '[object Array]') {
angular.forEach(value, function(subvalue) {
if(typeof subvalue === 'object') {
renderedHTML += renderTemplate(
subvalue.w.substring(4), renderLayout(subvalue.d)
);
} else {
renderedHTML += renderTemplate(subvalue.substring(4));
}
});
} else {
if(value > 12) {
value = 12;
} else if (value <= 0) {
value = 1;
}
subWidth = Math.floor(12 / value);
for (var i=0; i< value-1; i++) {
renderedHTML += renderTemplate(subWidth);
}
renderedHTML += renderTemplate((12-subWidth*(value-1)));
}
renderedHTML += '</div>';
rowCnt++;
});
return renderedHTML;
};
scope.$watch(attrs.hrLayout, function(value) {
element.html(renderLayout(value.data));
});
element.addClass("hr-layout");
};
}
};
}]);

This may help - http://jsfiddle.net/PwNZ5/1/
App.directive('hrLayout', function($compile) {
return {
restrict: 'A',
// allows transclusion
transclude: true,
// transcludes the content of an element on which hr-layout was placed
template: '<div ng-transclude></div>',
compile: function(tElement, tAttrs, transcludeFn) {
return function (scope, el, tAttrs) {
var data = scope.$eval(tAttrs.hrLayout),
dom = '';
transcludeFn(scope, function cloneConnectFn(cElement) {
// hide the transcluded content
tElement.children('div[ng-transclude]').hide();
// http://ejohn.org/blog/how-javascript-timers-work/‎
window.setTimeout(function() {
for(var row = 0; row < data.data.length; row++) {
dom+= '<div class="row">';
for(var col = 0; col < data.data[row]; col++) {
dom+= '<div class="col-md-' + data.data[row] + '">' + tElement.children('div[ng-transclude]').children(':eq(' + ( row + col ) + ')').html() + '</div>';
}
dom+= '</div>';
}
tElement.after(dom);
}, 0);
});
};
}
};
});

Your approach looks like too complex. Maybe you should use ngRepeat directive http://docs.angularjs.org/api/ng.directive:ngRepeat and orderBy filter http://docs.angularjs.org/api/ng.filter:orderBy insted of updating html of element every time when hrLayout was updated?

Related

Modifying elements with same directive

I have several elements in a container. One of the rows has two icons in it: zoom in and zoom out. When you click Zoom In, I'd like all the row's widths to grow.
<div id="events">
<year>year 1</year>
<year>year 2</year>
<year>year 3</year>
<year>year 4</year>
<div id="scaling">
<md-icon aria-label="Zoom In" class="material-icons" ng-click="zoomIn()">zoom_in</md-icon>
<md-icon aria-label="Zoom Out" class="material-icons" ng-click="zoomOut()">zoom_out</md-icon>
</div>
</div>
I have a year directive:
angular.module("app").directive("year", ['$rootScope', function ($rootScope) {
return {
link: function($scope, element, attr) {
var events = element;
$scope.zoomIn = function(ev) {
console.log('zoomin');
$scope.zoom = $scope.zoom + $scope.scale;
if($scope.zoom < 100) { $scope.zoom = 100; }
events.html($scope.zoom);
events.css({
'width': $scope.zoom + '%'
});
}
$scope.zoomOut = function(ev) {
$scope.zoom = $scope.zoom - $scope.scale;
if($scope.zoom < 100) { $scope.zoom = 100; }
events.css({
'width': $scope.zoom + '%'
});
}
}
}
}]);
However the width is only applied to the very last year element. Why is that?
You are overwriting the scope every time. So each instance of your year directive is clobbering the zoomIn and zoomOut methods each time it is instantiated.
Normally you could solve this by using a new or isolate scope in your directive definition object:
//new scope
{
scope: true
}
//isolate scope
{
scope: {}
}
However, since you want to bind click handlers outside your individual year directives you will have to do something else.
A better solution would be to pass in the attributes and simply respond to their changes:
return {
scope: {
zoom: '='
},
link: function(scope, elem, attrs){
scope.$watch('zoom', function(){
//Do something with 'scope.zoom'
});
}
};
Now your external zoomIn and zoomOut functions can just modify some zoom property on the parent scope, and you can bind your year components to that.
<year zoom="myZoomNumber"></year>
And just for posterity, here is a working snippet.
function EventsController() {
var $this = this;
var zoom = 1;
$this.zoom = zoom;
$this.zoomIn = function() {
zoom *= 1.1;
$this.zoom = zoom;
console.log({
name: 'zoomIn',
value: zoom
});
};
$this.zoomOut = function() {
zoom *= 0.9;
$this.zoom = zoom;
console.log({
name: 'zoomOut',
value: zoom
});
};
}
function YearDirective() {
return {
restrict: 'E',
template: '<h1 ng-transclude></h1>',
transclude: true,
scope: {
zoom: '='
},
link: function(scope, elem, attr) {
var target = elem.find('h1')[0];
scope.$watch('zoom', function() {
var scaleStr = "scale(" + scope.zoom + "," + scope.zoom + ")";
console.log({
elem: target,
transform: scaleStr
});
target.style.transform = scaleStr;
target.style.transformOrigin = 'left';
});
}
};
}
var mod = angular.module('my-app', []);
mod
.controller('eventsCtrl', EventsController)
.directive('year', YearDirective);
.scaling{
z-index:1000;
position:fixed;
top:10px;
left:10px;
}
.behind{
margin-top:50px;
z-index:-1;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="my-app" ng-controller="eventsCtrl as $ctrl">
<div class="scaling">
<button type="button" aria-label="Zoom In" ng-click="$ctrl.zoomIn()">zoom_in</button>
<button type="button" aria-label="Zoom Out" ng-click="$ctrl.zoomOut()">zoom_out</button>
</div>
<div class="behind">
<year zoom="$ctrl.zoom">year 1</year>
<year zoom="$ctrl.zoom">year 2</year>
<year zoom="$ctrl.zoom">year 3</year>
<year zoom="$ctrl.zoom">year 4</year>
</div>
</div>
The events.css is getting over-ridden, thus making it apply only to last element.
events.css({
'width': $scope.zoom + '%'
}).bind(this);
You have to bind it to current scope.

ng-repeat with ng-bind-html as pre and post-markup

I have an array with multiple objects, similar to this:
[
{ title: 'abc', 'pre': '<div class="class1"><div class="class2">', 'post': '</div>' },
{ title: 'def', 'pre': <div class="class3">', 'post': '</div>' },
{ title: 'ghi', 'pre': '<div class="class3">', 'post': '</div></div>' }
]
<div ng-repeat="item in myVar">
<div ng-bind-html="item.pre" />{{ item.title }}<div ng-bind-html="item.post" />
</div>
The above does not work (I have to open two div's in one, and close in two other items in that array, as illustrated above). The problem is that ng-bind-html needs to be bound to an element, which I cannot use, neither does a filter work:
<div ng-repeat="item in myVar">
{{ item.pre | trust }}{{ item.title }}{{ item.post | trust }}
</div>
angular.module('myModule').filter('trust', ['$sce',function($sce) {
return function(value, type) { return $sce.trustAsHtml; }
}]);
Any ideas?
You'll have to perform the concatenation pre-view, trust that (or turn on ngSanitize, potentially better-yet), then inject it.
As far as I know, there's no way to inject a partial HTML element the way you're trying to.
In your controller:
$scope.items = [...];
for (var i = 0; i < $scope.items.length; i++) {
var e = $scope.items[i];
e.concatenated = $sce.trustAsHtml(e.pre + e.title + e.post);
}
Then in your view:
<div ng-repeat="item in items">
<div ng-bind-html="item.concatenated" />
</div>
Of course, you'll probably want ngSanitize turned on, just to avoid any issues with e.title. That is, if someone entered a title of <script>alert('ahh!')</script>, that would end up being trusted.
Your version did not work because of how ngBindHtml is written:
var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
return {
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.ngBindHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
// we re-evaluate the expr because we want a TrustedValueHolderType
// for $sce, not a string
element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
});
};
}
};
}];
It injects using element.html(...), which needs a complete HTML element.

Angularjs responsive directive live updating issue (possibly due to ng-repeating the directive)

I am creating a post feed by ng-repeating JSON files from the cloud. I tried to make the posts responsive by using angular directives that update the template url with the screen size.
The problem is that only the last post in the ng-repeat responds and changes templates (with or without the reverse filter) when I resize the page. The other posts just remain the template that it was when originally loaded.
Here's the ng-repeat in the page
<div ng-show="post_loaded" ng-repeat="post in posts | reverse | filter:searchText ">
<feed-post>
</feed-post>
</div>
Here's the directive javascript file
app.directive('feedPost', function ($window) {
return {
restrict: 'E',
template: '<div ng-include="templateUrl"></div>',
link: function(scope) {
$window.onresize = function() {
changeTemplate();
scope.$apply();
};
changeTemplate();
function changeTemplate() {
var screenWidth = $window.innerWidth;
if (screenWidth < 768) {
scope.templateUrl = 'directives/post_mobile.html';
} else if (screenWidth >= 768) {
scope.templateUrl = 'directives/post_desktop.html';
}
}
}
};});
This happens because you re-assigning the .onresize in each directive and it stays effective only for the last linked directive.
I'd suggest to use it in a more angular way. You don't actually need a custom directive
In the controller that manages list of posts add reference to $window in $scope
$scope.window = $window;
Then in template make use of it
<div ng-include="directives/post_mobile.html" ng-if="window.innerWidth < 768"></div>
<div ng-include="directives/post_desktop.html" ng-if="window.innerWidth >= 768"></div>
To avoid extra wrappers for posts feed you might want to use ng-repeat-start, ng-repeat-end directives
this is a directive i wrote based on bootstrap sizes and ngIf directive :
mainApp.directive("responsive", function($window, $animate) {
return {
restrict: "A",
transclude: 'element',
terminal: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
//var val = $attr["responsive"];
var block, childScope;
$scope.$watch(function(){ return $window.innerWidth; }, function (width) {
if (width < 768) {
var s = "xs";
} else if (width < 992) {
var s = "sm";
} else if (width < 1200) {
var s = "md";
} else {
var s = "lg";
}
console.log("responsive ok?", $attr.responsive == s);
if ($attr.responsive == s) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end responsive: ' + $attr.responsive + ' ');
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
block.clone.remove();
block.clone = null;
block = null;
}
}
});
}
};
});

Drawing morris chart in angular directive almost shows up

I'm trying to draw a morris chart in an angular directive that is within an ng-repeat block. It is weird, because it draws, almost? I can see it's there and the mouseovers work, but the graph itself is only a thin line at the top. Does anybody have any ideas?
Here's the html:
<div id="page-wrapper" ng-repeat="d in dealerGroup.Dealerships__r" ng-if="expandedDealer == d.Id">
<div class="panel-heading">Area Chart Example</div>
<div class="panel-body">
<area-chart dealership="d" chartData="d.SalesChartData"></area-chart>
</div>
</div>
And here's the directive
angular.module('areaChart', ['ui.bootstrap']).directive('areaChart', function($window) {
var directive = {};
// directive.templateUrl = directivePath + '/charts/area-chart.html';
directive.restrict = 'EA';
directive.scope = {
dealership: "=",
chartdata: "="
};
directive.controller = function($scope) {
$scope.ykeys = function() {
var ykeys = [];
angular.forEach($scope.chartdata, function(d,k) {
angular.forEach(d, function(value,key) {
if(key != 'period') { ykeys.push(key); }
})
});
return ykeys;
}
}
directive.link = function($scope,element,attrs) {
Morris.Area({
element: element,
xkey: 'period',
ykeys: $scope.ykeys(),
labels: $scope.ykeys(),
hideHover: 'auto',
pointSize: 2,
data: $scope.chartdata
});
}
return directive;
});
And here's what happens:
Additionally, resizing makes the whole thing blow up with javascript errors everywhere. But i'll worry that separately

generating a variable-length dropdown based on input value in an AngularJS directive

I want to provide a page selection directive that generates "Page [ 1 ] of x". The number of pages in the dropdown is dependent upon values passed into the directive, so it can't be part of a static template. I'm having a difficult time figuring out how/where to generate the <select><option>...</select>.
I have tried, unsuccessfully, to do it via:
an $observe (and $watch) in link
a function added to $scope in controller, which returns $compile(markup)($scope) (This gives the error Error: [$parse:isecdom] Referencing DOM nodes in Angular expressions is disallowed!)
a sub-directive for the <select> element (The link $observer never seemed to get the recordCount updates, regardless of inherited or shared scope.)
ng-repeat in the template
Here's my mangled code, as it currently stands.
HTML
<x-pager
record-count="{{recordCount}}"
page-size="pageSize"
page-number="pageNumber"
set-page="selectPage(page)"
></x-pager>
JS
module.directive("pager", ["$compile",
function ($compile)
{
return {
template: "<div class='pager' ng-show='recordCount > pageSize'>\
{{recordCount}} results\
<button>« Prev</button>\
page <select>\
<option>#</option>\
</select> of {{calcPages()}}\
<button>Next »</button>\
</div>",
replace: true,
restrict: "E",
scope: {
recordCount: "#",
pageSize: "=",
pageNumber: "=",
setPage: "&"
},
link: function (scope, element, attrs)
{
/*
* We can't build the page selection dropdown until
* we know how many records we have. Register an
* observer to do this when recordCount changes.
*/
attrs.$observe("recordCount", function (recCnt)
{
var html;
var pages;
var i;
if (angular.isDefined(recCnt)) {
html = "<select>\n";
pages = Math.ceil(scope.recordCount / scope.pageSize);
for (i=1; i<=pages; i++) {
html += " <option value='" + i + "'>" + i + "</option>\n";
}
html += "</select>";
console.log("generatePageSelect html", html);
html = $compile(html)(scope);
// add the template content
// angular.element("select.page-selector").html(html);
// template: page <select class='page-selector'></select> of {{calcPages()}}\
}
});
},
controller: function ($scope)
{
$scope.calcPages = function ()
{
return Math.ceil($scope.recordCount / $scope.pageSize);
};
function genPagesArray ()
{
var pages = $scope.calcPages();
var i;
var pagesArray = [];
for (i=0; i<pages; i++) {
pagesArray.push(i);
}
return pagesArray;
}
$scope.pagesArray = genPagesArray();
console.log("$scope.pagesArray", $scope.pagesArray);
// template: page {{generatePageSelect()}} of {{calcPages()}}\
$scope.generatePageSelect = function ()
{
var html = "<select>\n";
var pages = $scope.calcPages();
var i;
for (i=1; i<=pages; i++) {
html += " <option value='" + i + "'>" + i + "</option>\n";
}
html += "</select>";
return $compile(html)($scope);
};
}
};
}
]);
To expand on my comment from earlier, here's a directive that does (most of) what you want it to do.
angular.module('Test', []).controller('TestCtrl', function($scope) {
$scope.pageSize = 10;
$scope.pageNumber = 1;
$scope.recordCount = 30;
}).directive("pager", function () {
return {
template: '<div class="pager" ng-show="recordCount > pageSize">\
{{recordCount}} results\
<button ng-click="pageNumber = pageNumber - 1" ng-disabled="pageNumber <= 1">« Prev</button>\
page <select ng-model="pageNumber" ng-options="i for i in pages"></select> of {{totalPages}}\
<button ng-click="pageNumber = pageNumber + 1" ng-disabled="pageNumber >= totalPages">Next »</button>\
</div>',
replace: true,
restrict: "E",
scope: {
recordCount: "#",
pageSize: "=",
pageNumber: "=",
setPage: "&"
},
link: function (scope, element, attrs) {
attrs.$observe("recordCount", function (count) {
if (angular.isDefined(count)) {
scope.recordCount = parseInt(count);
var i;
scope.totalPages = Math.ceil(scope.recordCount / scope.pageSize);
scope.pages = [];
for (i=1; i<=scope.totalPages; i++) {
scope.pages.push(i);
}
}
});
}
}
});
Plunkr here.

Resources