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
Related
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.
I am facing problem with infinite loop on loading the view. The data is loaded from an API call using ngResource in the controller. The view seems to be reloaded multiple times before rendering the view correctly. I use ng directives in the template calling scope methods and this seems to get into loop causing the view to be re-rendered.
Here is my Controller
.controller('IndexCtrl', ['$scope', '$stateParams', 'ProfileInfo',
function($scope, $stateParams, ProfileInfo) {
$scope.navTitle = 'Profile Information';
$scope.data = {};
ProfileInfo.query({
id: $stateParams.id
}).$promise.then(function(Profile) {
if (Profile.status == 200) {
$scope.data.Profile = Profile.data[0];
}else{
console.log(Profile.status);
}
}, function(error) {
console.log(error);
});
$scope.showImageBlock = function(object, image) {
if (object.hasOwnProperty('type') && object.type == 'image') {
imageReference = object.value;
var imageUrl;
angular.forEach(image, function(value, key) {
if (value.id == imageReference) {
$scope.data.imageUrl = value.graphic.url;
return;
}
});
}
return object.hasOwnProperty('type') && object.type == 'image';
};
$scope.showText = function(object) {
console.log('text');
return object.hasOwnProperty('type') && object.type == 'text';
};
}
])
And Here is my template
<ion-view cache-view="false">
<ion-nav-title>
{{navTitle}}
</ion-nav-title>
<div class="bar bar-subheader bar-light">
<h2 class="title">{{navSubTitle}}</h2>
</div>
<ion-content has-header="true" padding="true" has-tabs="true" class="has-subheader">
<div ng-repeat="profileInfo in data.Profile">
<div class="list">
<img ng-if="showImageBlock(profileInfo,data.Profile.images)" ng-src="{{ data.imageUrl }}" class="image-list-thumb" />
<div ng-if="showText(profileInfo)">
<a class="item">
{{profileInfo.name}}
<span ng-if="profileInfo.description.length != 0"class="item-note">
{{profileInfo.description}}
</span>
</a>
</div>
</div>
</div>
</ion-content>
Here is the output of console window when tried log the number of times showText function is called.
The actual result from ngResource call has only 9 items in array but it loops more than 9 times and also multiple loops. This happens for a while and stops. Could anyone please point me in the right direction in fixing it.
Thank you
Finally I ended up creating a custom directive which does the function of ng-if without the watchers which triggers the digest loop. It's not a pretty solution but it seems to do the job as expected. I copied the code of ng-if and removed the $scope watcher. Here is the custom directive.
angular.module('custom.directives', [])
.directive('customIf', ['$animate',function($animate) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
var block, childScope, previousElements;
value = $scope.$eval($attr.customIf);
if (value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end customIf: ' + $attr.customIf + ' ');
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
}
else {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
}
}
}
};
}]);
This allows us to use the customIf as follows
<div custom-if="showText(profileInfo)">
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;
}
}
});
}
};
});
I am trying to have a directive with a repeat on it and have it call a function on the parent control as well as child controls. however when I add a scope: { function:&function}
the repeat stops working properly.
fiddle
the main.html is something like
<div ng-app="my-app" ng-controller="MainController">
<div>
<ul>
<name-row ng-repeat="media in mediaArray" on-delete="delete(index)" >
</name-row>
</ul>
</div>
</div>
main.js
var module = angular.module('my-app', []);
function MainController($scope)
{
$scope.mediaArray = [
{title: "predator"},
{title: "alien"}
];
$scope.setSelected = function (index){
alert("called from outside directive");
};
$scope.delete = function (index) {
alert("calling delete with index " + index);
}
}
module.directive('nameRow', function() {
return {
restrict: 'E',
replace: true,
priority: 1001, // since ng-repeat has priority of 1000
controller: function($scope) {
$scope.setSelected = function (index){
alert("called from inside directive");
}
},
/*uncommenting this breaks the ng-repeat*/
/*
scope: {
'delete': '&onDelete'
},
*/
template:
' <li>' +
' <button ng-click="delete($index);">' +
' {{$index}} - {{media.title}}' +
' </button>' +
' </li>'
};
});
As klauskpm said is better to move common logic to an independent service or factory. But the problem that i see is that the ng-repeat is in the same element of your directive. Try embed your directive in an element inside the loop and pass the function in the attribute of that element or create a template in your directive that use the ng-repeat in the template
<li ng-repeat="media in mediaArray" >
<name-row on-delete="delete(media)" ></name-row>
</li>
As I've suggested you, the better approach to share methods is building a Factory or a Service, just like bellow:
app.factory('YourFactory', function(){
return {
setSelected: function (index){
alert("called from inside directive");
}
}
};
And you would call it like this:
function MainController($scope, YourFactory) {
$scope.setSelected = YourFactory.setSelected;
// Could even use $scope.yf = YourFactory;, and call yf.setSelected(index);
// at your view.
(...)
module.directive('nameRow', function(YourFactory) {
(...)
$scope.setSelected = YourFactory.setSelected;
(...)
Hope it will help you.
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?