How to pass dynamic object in angularjs directive - angularjs

I am trying to generate multiple chart on single page with ng-repeat. The directive to achieve this accept chart data object as below.
module.directive('miChart', function () {
return {
restrict: 'E',
template: '<div></div>',
scope: {
chartData: "=value",
chartObj: "=?"
},
replace: true,
link: function ($scope, $element, $attrs) {
$scope.$watch('chartData', function (value) {
if (!value) {
return;
}
$scope.chartData.chart.renderTo = $scope.chartData.chart.renderTo || $element[0];
$scope.chartObj = new Highcharts.Chart($scope.chartData);
});
}
}
});
The basic html is as below.
<table>
<tr>
<td>
<mi-chart value="pieChartData" chart-obj="pieChartObj"></mi-chart>
</td>
<td>
<mi-chart ng-repeat="property in Property.Competitors" value="property.PropertyId" chart-obj="property.PropertyId"></mi-chart>
</td>
</tr>
</table>
Javascript code to assign values to scope.
dataPromise.then(function (serverResponse) {
var pieChart = serverResponse.data;
var pieChartData = getBaseChartData(pieChart.Primary);
var chartData = jQuery.extend(true, {}, pieChartData);
chartData = jQuery.extend(true, chartData, chartDiff);
$scope.pieChartData = chartData;
if (pieChart.Competitors.length > 0)
{
jQuery.each(pieChart.Competitors, function (index, CompData) {
var pieChartDataComp = getBaseChartData(CompData);
var chartDataComp = jQuery.extend(true, {}, pieChartDataComp);
chartDataComp = jQuery.extend(true, chartDataComp, chartDiff);
$scope[CompData.PropertyId] = chartDataComp;
}
});
}
});
In above code i am able to render my pieChartData with my directive.
But the code doesnt work in case of ng-repeat where i have to create dynamic object based on values in "Property.Competitors".
I can not use string(#) or one way binding(&) in my case as i have to deal with chart(highchart) object and watch functionality.
I am assigning dynamic scope values in my service with server side calls.
Can anybody help me out to generate multiple chart with ng-repeat and directive.
How can i create dynamic object and keep watch on that?

Related

AngularJS: Cloning element containing ng-repeat

I am creating a directive with Angular 1.6 which creates a fixed header for a table. I am trying to achieve this by clone the table header and fixing this. This all works fine in most of my tables. I am using scope: false to keep parent scope, as some header elements references e.g. sorting functions. This also works. My problem is with one table I have which creates columns based on an array, because I want to be able to change columns. The columns are added with ng-repeat. When I clone this header, the ng-repeat is not cloned.
What can I do to clone an element containing an ng-repeat?
HTML of table:
<table class="proloen-table no-last-border" table-fix-header>
<thead class="light-blue-background">
<tr>
<th>{{vm.testString}}</th>
<th ng-repeat="head in vm.tableHeaders">
<span>{{ head.label | translate }}</span>
<sorting sortkey="head.sort" color="'white'" filter="vm.state.sorting"></sorting>
</th>
</tr>
</thead>
...
</table>
Controller (with controllerAs: 'vm') has (among other things):
vm.testString = 'Test';
vm.tableHeaders = [{label: 'Column1', sort: 'prop1'}, {label: 'Column2', sort: 'prop2'}];
The directive is as follows:
.directive('tableFixHeader', function ($window, $compile) {
return {
restrict: 'A',
scope: false,
link: function (scope, element) {
var clone;
function init() {
element.wrap('<div class="fix-table-container"></div>');
clone = element.clone(true);
clone.find('tbody').remove().end().addClass('table-header-fixed');
clone.removeAttr('table-fix-header');
$compile(clone)(scope);
element.before(clone);
resizeFixed();
}
function resizeFixed() {
clone.find('th').each(function (index) {
$(this).css('width', element.find('th').eq(index).outerWidth() + 'px');
});
}
function scrollFixed() {
var offset = $($window).scrollTop(),
tableOffsetTop = element.offset().top,
tableOffsetBottom = tableOffsetTop + element.height() - element.find('thead').height();
if (offset < tableOffsetTop || offset > tableOffsetBottom){
clone.hide();
}
else if (offset >= tableOffsetTop && offset <= tableOffsetBottom && clone.is(':hidden')) {
clone.show();
}
}
$window.addEventListener('scroll', scrollFixed);
$window.addEventListener('resize', resizeFixed);
scope.$on('$destroy', function() {
$window.removeEventListener('scroll', scrollFixed);
$window.removeEventListener('resize', resizeFixed);
});
init();
}
};
});
The directive works fine for tables columns are fixed and the above example clones the first "hardcoded" column just fine, along the variable from controller. The problem arises when cloning the ng-repeat. I just can't seem to figure out how to clone the ng-repeat, so that it to will work and update when I update the list of columns.
You could try to send event with $scope.$emit when ng-repeat finished rendering. Or create own event emitter and connect your directives;
app.directive('onFinishRepeat', function(){
return {
restrict: 'A',
link: function($scope) {
if($scope.$last == true) {
$scope.$emit('ng-repeat', 'finish');
}
}
}
})
app.directive('tableFixHeader', function ($window, $compile) {
return {
restrict: 'A',
scope: false,
link: function (scope, element) {
var clone;
function init() {
element.wrap('<div class="fix-table-container"></div>');
clone = element.clone(true);
clone.find('tbody').remove().end().addClass('table-header-fixed');
clone.removeAttr('table-fix-header');
$compile(clone)(scope);
element.before(clone);
resizeFixed();
}
function resizeFixed() {
clone.find('th').each(function (index) {
$(this).css('width', element.find('th').eq(index).outerWidth() + 'px');
});
}
function scrollFixed() {
var offset = $($window).scrollTop(),
tableOffsetTop = element.offset().top,
tableOffsetBottom = tableOffsetTop + element.height() - element.find('thead').height();
if (offset < tableOffsetTop || offset > tableOffsetBottom){
clone.hide();
}
else if (offset >= tableOffsetTop && offset <= tableOffsetBottom && clone.is(':hidden')) {
clone.show();
}
}
$window.addEventListener('scroll', scrollFixed);
$window.addEventListener('resize', resizeFixed);
scope.$on('$destroy', function() {
$window.removeEventListener('scroll', scrollFixed);
$window.removeEventListener('resize', resizeFixed);
});
$scope.on('ng-repeat', function(event, data){
if(data == 'finish') {
init();
}
})
}
};
});
HTML
<table class="proloen-table no-last-border" table-fix-header>
<thead class="light-blue-background">
<tr>
<th>{{vm.testString}}</th>
<th ng-repeat="head in vm.tableHeaders" on-finish-repeat>
<span>{{ head.label | translate }}</span>
<sorting sortkey="head.sort" color="'white'" filter="vm.state.sorting"></sorting>
</th>
</tr>
</thead>
...
</table>

AngularJs filtering on click using a custom directive

I am trying to filter a list in using a click event in custom directive the filter works fine but how do I assign the filtered items so it is reflected in ng-repeat? Right now the list generated using ng-repeat on quesCompletedHeaders is not changed after the click event has taken place
app.directive('sectionSelector', ['$filter', 'questionnaireSections',
function ($filter, questionnaireSections) {
return {
restrict: 'A',
link: function (scope, iElement, iAttrs) {
iElement.on('click', function () {
scope.$apply(function () {
// filters correct question range
var filtered = $filter('rangeFilter')(scope.quesCompletedHeaders, [5, 10], true);
console.log(filtered); //=> This is correct how to put back on scope?
// There is an ng-repeat on quesCompletedHeaders in the template
// but nothing changes even when assigning filted results back
// on scope
scope.quesCompletedHeaders = filtered;
// works with $parent not sure why a child scope is
// created with the directive
scope.$parent.quesCompletedHeaders = filtered
});
});
}
};
}
]);
Ok I found out the reason sectionSelector is creating a child scope which should not be since scope:false is the default? why is this happening?
using scope.$parent.quesCompletedHeaders = filtered; solves the issue
Add scope 'quesCompletedHeaders' to directive
HTML
<hello-world sectionSelector="filtered.quesCompletedHeaders"></hello-world>
Javascript
app.controller('MainCtrl', function($scope) {
$scope.filtered.quesCompletedHeaders = {};
});
app.directive('sectionSelector', ['$filter', 'questionnaireSections',
function ($filter, questionnaireSections) {
return {
restrict: 'EA',
scope: {
questionnaireSections: "=",
},
link: function (scope, iElement, iAttrs) {
iElement.on('click', function () {
scope.$apply(function () {
// filters correct question range
var filtered = $filter('rangeFilter')(scope.quesCompletedHeaders, [5, 10], true);
console.log(filtered);
scope.filtered.quesCompletedHeaders = filtered;
});
});
}
};
}
]);
There is an easy alternative instead of directive.
you can have ng-click on the element and apply filter in controller to the list.
In HTML,
<input type="button" value="select section" ng-click="filterquestionaire(5,10)"/>
in Controller,
$scope.filterquestionaire = function(start,end) {
var filtered = $filter('rangeFilter')(scope.quesCompletedHeaders, [start, end], true);
$scope.quesCompletedHeaders = filtered;
};
OK so here is my final solution.
$scope.rangeFilter = function (range) {
$scope.quesRangeStart = range[0];
$scope.quesRangeEnd = range[1];
}
In the template I am piping the filter
ng-repeat="question in quesCompletedHeaders|rangeFilter:[quesRangeStart, quesRangeEnd]"
and on the filter buttons
ng-repeat="section in questionnaireSections"
ng-click="rangeFilter(section.range)"
and filter code
app.filter('rangeFilter', function () {
return function (items, rangeInfo) {
var min = parseInt(rangeInfo[0]);
var max = parseInt(rangeInfo[1]);
var filtered = items.filter(function (header) {
return header.rang >= min && header.rang <= max;
});
return filtered;
};
});

Issue with Popover AngularJS

I have a bunch of table rows which include inputs and buttons, namely. I would like to have a Popover display to the right of an input for a row if the value isn't matching the requirements defined. The button will also be disabled until the value of the input is correct.
Relevant HTML:
<div class="row col-md-4">
<table ng-controller="TestController" style="width: 100%">
<tr ng-repeat="element in model.InvoiceNumbers">
<td><input ng-model="element.id"
popover="Invoice must match ##-####!"
popover-placement="right"
popover-trigger="{{ { false: 'manual', true: 'blur'}[!isValidInvoice(element.id)] }}"
popover-title="{{element.id}}"/></td>
<td>{{element.id}}</td>
<td><button ng-disabled="!isValidInvoice(element.id)">Approve</button></td>
</tr>
</table>
</div>
Relevant JavaScript:
app.controller("TestController", function ($scope) {
$scope.model = {
InvoiceNumbers : [
{ id: '12-1234' },
{ id: '12-1235' },
{ id: '1234567' },
{ id: '1' },
{ id: '' }],
};
$scope.isValidInvoice = function (invoice) {
if (invoice == null) return false;
if (invoice.length != 7) return false;
if (invoice.search('[0-9]{2}-[0-9]{4}') == -1) return false;
return true;
};
});
The button gets disabled correctly on my local solution. However, I can't get the Popover to work; it behaves as if the model in its scope isn't getting updated. So, I looked through several links here (though most were from 2013 so I'd imagine a bit has changed) and their problems seemed to be solved by removing primitive binding. That didn't fix anything here. I added some console.log() lines in the function getting called from the Popover, and it was getting the correct value from the model each time. I also added a title to the Popover to show that its seeing the right value from the model.After seeing the log showing that it should be working correctly, I've run out of ideas.
The issue is element.id isn't updating dynamically within the trigger (it keeps its initial value, unlike popover-title which updates with the model). Is there something I did wrong?
Also, I've only been working with angular for a day so if you all have any suggestions on better ways to accomplish this, I'm open to suggestions.
Plunker: http://plnkr.co/edit/tiooSxSDgzXhbmIty3Kc?p=preview
Thanks
Found a solution on the angular-ui github page that involved adding these directives:
.directive( 'popPopup', function () {
return {
restrict: 'EA',
replace: true,
scope: { title: '#', content: '#', placement: '#', animation: '&', isOpen: '&' },
templateUrl: 'template/popover/popover.html'
};
})
.directive('pop', function($tooltip, $timeout) {
var tooltip = $tooltip('pop', 'pop', 'event');
var compile = angular.copy(tooltip.compile);
tooltip.compile = function (element, attrs) {
var parentCompile = compile(element, attrs);
return function(scope, element, attrs ) {
var first = true;
attrs.$observe('popShow', function (val) {
if (JSON.parse(!first || val || false)) {
$timeout(function () {
element.triggerHandler('event');
});
}
first = false;
});
parentCompile(scope, element, attrs);
}
};
return tooltip;
});
And here's the changes I made to the controller and view to make it work like I wanted in the original question:
<div class="row col-md-4">
<table ng-controller="TestController" style="width: 100%">
<tr ng-repeat="element in model.InvoiceNumbers">
<td><input ng-model="element.id"
pop="Invoice must match ##-####!"
pop-placement="right"
pop-show="{{element.showPop}}"
ng-blur="isValidInvoice($index, $event)" /></td>
<td>{{element.id}}</td>
<td><button ng-disabled="!isValidInvoice($index)">Approve</button></td>
</tr>
</table>
</div>
JavaScript:
app.controller("TestController", function ($scope) {
$scope.model = {
InvoiceNumbers: [
{ id: '12-1234', showPop: false },
{ id: '12-1235', showPop: false },
{ id: '1234567', showPop: false },
{ id: '1', showPop: false },
{ id: '', showPop: false }]
};
$scope.isValidInvoice = function ($index, $event) {
var obj = $scope.model.InvoiceNumbers[$index];
var isValid = function () {
if (obj.id === null) return false;
if (obj.id.length != 7) return false;
if (obj.id.search('[0-9]{2}-[0-9]{4}') == -1) return false;
return true;
};
if ($event != null && $event.type == "blur") obj.showPop = !isValid();
return isValid();
};
});
Plunker: http://plnkr.co/edit/5m6LHbapxp5jqk8jANR2?p=preview

AngularJS Passing Variable to Directive

I'm new to angularjs and am writing my first directive. I've got half the way there but am struggling figuring out how to pass some variables to a directive.
My directive:
app.directive('chart', function () {
return{
restrict: 'E',
link: function (scope, elem, attrs) {
var chart = null;
var opts = {};
alert(scope[attrs.chartoptions]);
var data = scope[attrs.ngModel];
scope.$watch(attrs.ngModel, function (v) {
if (!chart) {
chart = $.plot(elem, v, opts);
elem.show();
} else {
chart.setData(v);
chart.setupGrid();
chart.draw();
}
});
}
};
});
My controller:
function AdListCtrl($scope, $http, $rootScope, $compile, $routeParams, AlertboxAPI) {
//grabing ad stats
$http.get("/ads/stats/").success(function (data) {
$scope.exports = data.ads;
if ($scope.exports > 0) {
$scope.show_export = true;
} else {
$scope.show_export = false;
}
//loop over the data
var chart_data = []
var chart_data_ticks = []
for (var i = 0; i < data.recent_ads.length; i++) {
chart_data.push([0, data.recent_ads[i].ads]);
chart_data_ticks.push(data.recent_ads[i].start);
}
//setup the chart
$scope.data = [{data: chart_data,lines: {show: true, fill: true}}];
$scope.chart_options = {xaxis: {ticks: [chart_data_ticks]}};
});
}
My Html:
<div class='row-fluid' ng-controller="AdListCtrl">
<div class='span12' style='height:400px;'>
<chart ng-model='data' style='width:400px;height:300px;display:none;' chartoptions="chart_options"></chart>
{[{ chart_options }]}
</div>
</div>
I can access the $scope.data in the directive, but I can't seem to access the $scope.chart_options data.. It's definelty being set as If I echo it, it displays on the page..
Any ideas what I'm doing wrong?
UPDATE:
For some reason, with this directive, if I move the alert(scope[attrs.chartoptions]); to inside the $watch, it first alerts as "undefined", then again as the proper value, otherwise it's always undefined. Could it be related to the jquery flot library I'm using to draw the chart?
Cheers,
Ben
One problem I see is here:
scope.$watch(attrs.ngModel, function (v) {
The docs on this method are unfortunately not that clear, but the first argument to $watch, the watchExpression, needs to be an angular expression string or a function. So in your case, I believe that you need to change it to:
scope.$watch("attrs.ngModel", function (v) {
If that doesn't work, just post a jsfiddle or jsbin.com with your example.

how to exec js after AngularJS ng-repeat finished

I'm new to AngularJS. I want to use ng-repeat to render a list of data.
Each of the data should have a <abbr class="timeago" title="2012-10-10 05:47:21"></abbr> alike after rendered. And then I could use jquery plugin timeago to turn it into human friendly text about 1 hour ago.
My code is as below. But it take no effect. Please help.
EDIT: My problem is that, I can get the right html rendered. But code in directive do not run.
the html:
<div ng-app='weiboApp' ng-controller="WeiboListCtrl">
<table><tbody>
<tr ng-repeat='w in weibo' weiboLister='w'>
<td>{{ w.text }}></td>
<td><abbr class="timeago" title="{{ w.created }}"></abbr></td>
</tr>
</tbody></table>
</div>
the js:
var module = angular
.module('weiboApp', [])
.directive('weiboLister', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
scope.watch('w', function (val) {
console.log(element); //this never run
element.find("abbr.timeago").timeago(); //this never run
}, true);
}
}
});
function WeiboListCtrl($scope, $http) {
$http.get('/api/weibo/list').success(function(data) {
$scope.weibo = data;
});
}
The problem turned out to be: should define directive with camel-case weiboLister and use it in html with snake-case weibo-lister. Thanks to #tosh shimayama.
The correct code as below: (I added a remove function in case you're looking for the same thing.)
the html:
<div ng-app='weiboApp' ng-controller="WeiboListCtrl">
<table><tbody>
<tr ng-repeat='w in weibo' weibo-lister='w'> <!--important to be snake-case here-->
<td>{{ w.text }}></td>
<td><abbr class="timeago" title="{{ w.created }}"></abbr></td>
<td><a ng-click='remove(w)'>×</a></td>
</tr>
</tbody></table>
</div>
the js:
var module = angular
.module('weiboApp', [])
.directive('weiboLister', function () {
function delete(id, s_function, f_function) {
//...
if success { s_function(); }
else { f_function(); }
}
return {
restrict: 'A',
link: function (scope, element, attr) {
scope.$watch('w', function (val) {
element.find("abbr.timeago").timeago();
}
scope.destroy = function(callback) {
deletenews(scope.w.id, function () {
//s_function, things you want to do when delete with success
element.fadeOut(400, function () {
//this callback will splice the element in model
if (callback) callback.apply(scope);
})
}, function () {
//f_function, when delete with failure
});
};
}
}
});
function WeiboListCtrl($scope, $http) {
$http.get('/api/weibo/list').success(function(data) {
$scope.weibo = data;
});
$scope.removeWeibo = function(w) {
var idx = $scope.weibo.indexOf(w);
if (idx !== -1) {
this.destroy(function() {
$scope.weibo.splice(idx, 1);
});
}
};
}

Resources