I am attempting to create a custom table element like this:
<datatable items='tableItems' columns='columnsConfig' />
Here 'tableItems' is my array of items and 'columnsConfig' is configuration for column rendering, something like :
$scope.tableItems = [...];
$scope.columnsConfig = [
{
name: 'check',
with: '20px',
renderer: function (rowItem, cellValue) {
return '<input ng-click="clickHandler()" type="checkbox"/>';
}
},
{name: "person.fullName", text: "Name", visible: true, width: '150px'},
{
name: "person.age",
text: "Age",
renderer: function(rowItem, cellValue) {
return cellValue + ' years old';
}
}
];
Inside renderer function I can specify some additional data processing or templating.
In my directive template I have this:
<tbody>
<tr ng-repeat="item in items">
<td ng-repeat="column in columns"
ng-show="column.visible"
ng-bind-html-unsafe="getCellValue(item, $index)">
</td>
</tr>
</tbody>
where inside 'getCellValue' function I invoking my renderer function. Here is directive code:
angular.module('components', [])
.directive('datatable', function () {
return {
restrict: 'E',
templateUrl: '../pages/component/datatable.html',
scope: {
items: "=",
columns: "="
},
controller: function ($scope, $element) {
$scope.getCellValue = function (item, columnIndex) {
var column = $scope.columns[columnIndex];
// return render function result if it has been defined
if (column.renderer) {
return column.renderer(item, getItemValueByColumnName(item, column.name));
}
// return item value by column
return getItemValueByColumnName(item, column.name);
};
}
}
});
All works fine except ng-... directives. I think I have to do some additional processing of 'renderer' function results via $compile or something but yet I can't figure out how to achieve this. So the question is how make ng directives work when I specify them via my renderer function ?
Thanks.
After some investigations I have found next solution:
//after all DOM manipulations we should recompile parts that has been modified
setTimeout(function () {
applyAfterRenderDOMChanges();
}, 0);
var applyAfterRenderDOMChanges = function () {
var cells = $('td', element).children();
$compile(cells)(scope);
scope.$apply();
};
I have some concerns about efficiency of this solition but it works well so far.
Related
I'm trying to keep all my model functionality in one place. I'd like to be able to call its methods within it:
JS
/**
* This app prints "OK" a number of times, according to which button is pressed
*/
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
$scope.okModel = {
oks: [],
addOne: function(){
this.oks.push(["OK"]);
},
addTwo: function(){
this.addOK();
this.addOK();
},
buttons: [
{name:"addOK", action: this.addOne}, // THIS is where the issue is I think
{name:"add2OKs", action: this.addTwo}
]
};
}]);
HTML
<div ng-controller="MyCtrl">
<!-- print the object holding all the "OK"s -->
oks: {{okModel.oks}}
<!-- show the buttons -->
<button ng-repeat="item in okModel.buttons" ng-click="item.action()">
{{item.name}}
</button>
<!-- print the "OK"s -->
<div ng-repeat="ok in okModel.oks">
{{ok[0]}}
</div>
</div>
I'm not getting an error, but it's not working either. No "OK"s are being added to the model. It seems like the issue may be with the okModel.buttons action property.
Here's a plunker: https://plnkr.co/edit/mDk43yEKSQB37QSmiKJn?p=preview
TL;DR: I realize that the issue is probably with this in buttons, what should I use instead?
Bonus question: I'm new to angular and realize I may be using models incorrectly. If you know of a better way to do models, please let me know.
Since you asked, the "angular" way to do this is to have a service provide the model, rather than defining it in your controller:
var app = angular.module('myApp', []);
app.factory('modelService', function() {
var okModel = {
oks: [],
}
okModel.addOne = function() {
okModel.oks.push(["OK"]);
};
okModel.addTwo = function() {
okModel.addOne();
okModel.addOne();
};
okModel.buttons = [{
name: "addOK",
action: okModel.addOne
},
{
name: "add2OKs",
action: okModel.addTwo
}
];
return okModel;
});
app.controller('MyCtrl', ['$scope', 'modelService', function MyCtrl($scope, modelService) {
$scope.okModel = modelService;
}]);
Here is a plunk.
#reeverd answer seems to be correct. Here is a little more clean up.
$scope.okModel = {
oks: [],
addOne: function(){
oks.push(["OK"]); // you don't have to push an array with the OK text. You can just push in the OK text itself.
},
addMulti: function(cnt){
for (var ii = 0; cnt < ii; ii++) {
// this.addOK(); this may also be your issue addOK function is not defined.
$scope.addOne(); // calls the addOne and pushes OK into the oks array.
}
},
buttons: [
{name:"addOK", action: $scope.addOne}, // THIS is where the issue is I think
{name:"add2OKs", action: $scope.addMulti(2)}
]
};
The problem is that this inside the {name:"addOK", action: this.addOne} in the array is the object itseld, and not the object wrapping the array. In this case you could do something like this:
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
// Here you declare the functions which are going to modify
// $scope.okModel.oks
var addOne = function() {
if ($scope.okModel) {
$scope.okModel.oks.push("OK");
}
},
addTwo = function() {
addOne();
addOne();
};
$scope.okModel = {
oks: [],
addOne: addOne, // Here you assign okModel.addOne to the functions declared above
addTwo: addTwo, // Same here
buttons: [{
name: "addOK",
action: addOne // Same here
}, {
name: "add2OKs",
action: addTwo // Same here
}]
};
}]);
First you declare the functions which will modify $scope.okModel.oks array and then you use that same funcions both in your model methods and in your model buttons.
EDIT: Here you have a working plunker: https://plnkr.co/edit/sKUxjzUyVsoYp3S7zjTb?p=preview
Instead of using this, try using $scope.okModel. The actual object where this refers to is not always what you expect. It depends on how the function is invoked.
EDIT:
You can pull the definition of the functions out of the okModel like this:
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
var addOne = function() {
$scope.okModel.oks.push(["OK"]);
};
var addTwo = function() {
addOne();
addOne();
};
$scope.okModel = {
oks: [],
addOne: addOne,
addTwo: addTwo,
buttons: [{
name: "addOK",
action: addOne
}, {
name: "add2OKs",
action: addTwo
}]
};
}]);
I am relatively new to Angular and got stuck on a custom directive.
I am trying to create a dynamic grid as a custom directive.
I already got that part working as in this example:
working grid as custom directive
There are certain scenarios where I need to set attributes on some of the elements of the grid.
This part has got me stumped.
I am planning on including the attributes as an array inside the object and then just putting it in the html tag of the associated entry.
This part is demonstrated here:
broken grid as custom directive with dynamic attributes
If you look at the "entries" array in the controller, I have now changed it to include an "attributes" array which will contain objects specifying the attribute name and property. These attributes should then be applied to the associated column.
e.g.
(First entry of the array)
col1: {
text: 'Obj1.col1',
attributes: [{
attr: 'ng-class',
attrVal: 'propVal == "" ? "someClass" : "someOtherClass"'
}, {
attr: 'id',
attrVal: '{{propName}}{{$index}}'
}]
},
...Truncated for brevity
This array entry should then be translated to:
<td ng-class="propVal == '' ? 'someClass' : 'someOtherClass'" id="col11">Obj1.col1</td>
I have read a couple of articles about the execution order of compile, controller, pre-link and post-link functions and have played around with different orders and trying to invoke compiling myself, but it all has failed.
Probably because I lack a deeper understanding of how it all ties together.
If someone can help me out or point me in the right direction if I'm heading down the wrong path, I would greatly appreciate that.
Okay, I finally figured out how to generate the grid dynamically using embedded custom directives inside a parent custom directive.
Here is a plunker showing how I did it:
Plunker with working dynamic grid
I have the Html templates defined as:
<div ng-grid ng-collection="entries" ng-collection-headings="headings" ng-button-click="theAction(inp)">
<div ng-checkbox-column></div>
</div>
and then the ng-grid directive as:
.directive("ngGrid", function () {
return {
restrict: "A",
scope: {
ngCollectionHeadings: "=",
ngCollection: "=",
ngButtonClick: "&"
},
template: function (element, attrs) {
var children = element.html();
children = children.trim().replace(/div/g, "td");
var htmlText = "<input type='button' ng-click='buttonClicked()' value='From the grid directive' /><table class='table table-bordered'><thead><tr><th ng-repeat='heading in ngCollectionHeadings'>{{heading}}</th></tr></thead><tbody><tr id='item{{$index}}' ng-repeat='item in ngCollection'>" + children + "</tr></tbody></table>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.buttonClicked = function (inp) {
if (typeof inp != 'undefined')
inp = inp + ", through the grid directive.";
else
inp = "From the grid directive.";
$scope.ngButtonClick({ inp: inp });
};
}
};
})
and finally the ng-checkbox-column directive:
.directive("ngCheckboxColumn", function () {
return {
restrict: "A",
template: function (element, attributes) {
var htmlText = "<td><label><input type='checkbox' ng-model='item.checked' ng-click='tempButtonClicked()' /> From the checkbox directive.</label></td>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.tempButtonClicked = function () {
var val = "From the checkbox directive";
$scope.buttonClicked(val);
};
}
};
})
My data collections are pretty straight forward:
$scope.headings = {
head1: 'Heading 1',
head2: 'Heading 2',
head3: 'Heading 3'
};
$scope.entries = [{
col1: 'Obj1.col1',
col2: 'Obj1.col2',
col3: 'Obj1.col3',
checked: false
}, {
col1: 'Obj2.col1',
col2: 'Obj2.col2',
col3: 'Obj2.col3',
checked: false
}, {
col1: 'Obj3.col1',
col2: 'Obj3.col2',
col3: 'Obj3.col3',
checked: false
}, {
col1: 'Obj4.col1',
col2: 'Obj4.col2',
col3: 'Obj4.col3',
checked: false
}];
This is still not entirely completed, but you should get the basic idea.
I am using morris.js charts in my angular js app.
I converted it to directive like that:
barchart.js:
angular.module('app_name').directive('barchart', function () {
return {
// required to make it work as an element
restrict: 'AEC',
template: '<div class=chart_div></div>',
replace: true,
// observe and manipulate the DOM
link: function ($scope, element, attrs) {
var data = $scope[attrs.data],
xkey = $scope[attrs.xkey],
ykeys = $scope[attrs.ykeys],
labels = $scope[attrs.labels];
Morris.Bar({
element: element,
data: data,
xkey: xkey,
ykeys: ykeys,
labels: labels,
hideHover: true,
grid: false
});
}
};
});
then, in my page.html I use the directive like that:
<section class="graphs" ng-controller="ChartController">
<div class="graphs_box">
<div class="graphs_box_title">My Orders</div>
<div class="chart_bg">
<div barchart xkey="xkey" ykeys="ykeys" labels="labels" data="MyData"></div>
</div>
</div>
The problem is, when I am adding the data to the chart in 'ChartController'
like that:
getChartData = function () {
$scope.xkey = 'X';
$scope.ykeys = ['Y'];
$scope.labels = ['Total Tasks', 'Out of Budget Tasks'];
$scope.PlannedChart = [
{ range: 'A', total_tasks: 20 },
{ range: 'B', total_tasks: 35 },
{ range: 'C', total_tasks: 100 },
{ range: 'D', total_tasks: 50 }
];
};
It works. But when I try to add data from DB (json formatted) like that:
getChartData = function () {
ChartsService.getCharts("orders").success(function (data) {
$scope.xkey = 'X';
$scope.ykeys = 'Y';
$scope.labels = ['Total Tasks', 'Out of Budget Tasks'];
$scope.OrdersChart = data.Val_Chart;
});
};
It doesn't work.
The data is fetched from DB - OK (I saw it while debugging).
Also, I noticed when I debugged that the code first goes to barchart.js whith 'undefined' data, and only after that to the service that fetches the data.
I guess this is because of getCharts("orders") working asynchronously here. You'd need to call setData(data) on the object, that Morris.Bar() returns. See the Morris.js Documenation
I fixed it, with putting all the attributes for the directive in a single object and using this as link-function:
link: function ($scope, element, attrs) {
var params = $scope[attrs.params];
angular.extend(params, {element: element});
var graph = Morris.Line(params);
var refresh = function(new_params) {
graph.setData(new_params.data);
}
$scope.$watchCollection(attrs.params, refresh);
}
$scope.$watchCollection(attrs.params,refresh); observes the params object on the scope and you just give this object a new data-array, after the async loading finished. This updates it.
This is the directive element:
<barchart params="sentin_chart"></barchart>
And this ist the object in the controller:
$scope.sentin_chart = {};
$scope.sentin_chart.data = [];
$scope.sentin_chart.xkey = 'day';
$scope.sentin_chart.ykeys = ['sent_in'];
$scope.sentin_chart.labels = ['Sent in Stories'];
As I said you just need to put the new data array into this object to update the graph $scope.sentin_chart.data = %NEWDATAARRAY%
I hope this makes any sense for you.
I'm building a simple app with AngularJS. The app make a async AJAX call to the server and the server returns an array like this:
{
paragraphs: [
{content: "content one"},
{content: "cnt two"},
{content: "random three"},
{content: "last one yeeaah"}
]
}
So I'm setting this content to the StorageService factory via my set method. Everything is fine here.
I'm using ng-repeat to render the results and JQuery UI sortable to be able to change the order of the elements. When an item is swapped my script is calling the StorageService.swap method and the element order in StorageService is updated, BUT ng-repeat isn't rerendering the change, but if I remove/add or change the content it's working. How I can force the angular to rerender the ng-repeat?
= JSFIDDLE =
http://jsfiddle.net/6Jzx4/3/
= Example =
When a swap occurs the ng-repeat should be rerendered, so the IDs are consecutive
= Code =
HTML
<div ng-controller="Test" sortable>
<div ng-repeat="item in data().paragraphs" class="box slide_content" id="{{$index}}">
{{item.content}}, ID: {{$index}}
</div>
<input type="button" ng-click="add()" value="Add">
</div>
JS
var App = angular.module("MyApp", []);
App.controller("Test", function($scope, StorageService) {
StorageService.set({
paragraphs: [
{content: "content one"},
{content: "cnt two"},
{content: "random three"},
{content: "last one yeeaah"}
]
});
$scope.data = StorageService.get;
$scope.add = StorageService.add;
});
App.directive("sortable", function(StorageService) {
return {
link: function(scope, element, attrs) {
$(element[0]).sortable({
cancel: ".disabled",
items: "> .slide_content:not(.disabled)",
start: function(e, t) {
t.item.data("start_pos", t.item.index());
},
stop: function(e, t) {
var r = t.item.data("start_pos");
if (r != t.item.index()) {
StorageService.sort($(this).sortable("toArray"));
}
}
});
}
};
});
App.factory('StorageService', function() {
var output = {};
return {
set: function(data) {
angular.copy(data, output);
return output;
},
get: function() {
return output;
},
add: function() {
output.paragraphs.push({
content: 'Content'
});
},
sort: function(order) {
var localOutput = [];
for (var j in order) {
var id = parseInt(order[j]);
localOutput.push(output.paragraphs[id]);
}
console.log('new order', localOutput);
output.paragraphs = localOutput;
return output;
}
};
});
Angular doesn't know you've changed the array. Executing your sort inside a scope.$apply() will address that.
Note that I've added a that variable since this changes meaning inside the apply.
var that = this;
scope.$apply(function() {
StorageService.sort($(that).sortable("toArray"));
}
But that fix uncovers other problems that appear to be caused by the interaction between the jQuery sortable and Angular (here's a fiddle that shows an attempt at resolving the problems but still has issues). These issues have been solved in Angular UI Sortable. So a good path forward may be to switch to this.
My question is related to the link How to handle Highcharts events from an AngularJS directive?. What if I want to have the highchart generated from dynamic data? my chart object is defined/configured as below,
chart: {
type: 'bar'
},
series: [{
name: 'A,B,C,D',
score: [1,2,2,3]
}],
legend: {
enabled: false
}
I want to feed the corresponding 'name' and 'score' data dynamically from json string obtained from Ajax request and is of the form,
[{"Name":"A","Score":1},{"Name":"B","Score":2}]
Please let me know if i need to provide any other details.
Many Thanks.
Re-framing the question:
I want to create a highchart using angular js. My javascript file is
var app = angular.module('charts', []);
app.directive('highchart', function () {
return {
restrict: 'E',
template: '<div></div>',
replace: true,
link: function (scope, element, attrs) {
scope.$watch(function () { return attrs.chart; }, function () {
if (!attrs.chart) return;
var charts = JSON.parse(attrs.chart);
$(element[0]).highcharts(charts);
});
}
};
});
app.controller('Ctrl', function ($scope, $http, $timeout) {
$scope.overSpeedWorstRecords = [];
$scope.handleOverSpeedWorstRecords = function (data, status) {
$scope.overSpeedWorstRecords = data;
}
$http.get('http://localhost:12345/abc/pqr').success($scope.handleOverSpeedWorstRecords).error("error message");
$timeout($scope.fetch, 1000);
$scope.renderChart = {
chart: {
type: 'bar'
},
series: [{
name: 'A,B,C,D',
score: [1,2,2,3]
}],
legend: {
enabled: false
}
};
});
I am getting my json data in overSpeedWorstRecords through an Ajax query ($http.get). Additionally, I have defined a chart object with 'name' and 'score' hardcoded. With this setup I am having the highchart loaded with hardcoded data and I am getting the json data as well which I can access in the HTML as follows,
<!DOCTYPE>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Dashboard Application</title>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="http://code.highcharts.com/highcharts.js"></script>
<script type="text/javascript" src="Scripts/DashboardCtrl.js"></script>
</head>
<body>
<section ng-app="charts">
<div ng-controller="Ctrl">
<highchart chart='{{renderChart}}'></highchart>
<table>
<tr ng-repeat="record in overSpeedWorstRecords">
<td>{{record.Name}}</td>
<td>{{record.Score}}</td>
</tr>
</table>
</div>
</section>
</body>
</html>
However, I want to feed the json data which I am getting through the Ajax call, to the chart object to create the bar chart dynamically.
Please let me know if I need to elaborate the problem further.
Solution to the Problem:-
I solved the problem by myself. I am posting this solution for my future reference and in case it helps somebody having the same requirement.
The javascript was modified so that the Name and Score could be accessed from the json data. They were stored in the 'name' and 'score' arrays which were passed to the chart option object in order to render the highchart.
var app = angular.module('charts', []);
app.directive('highchart', function () {
return {
restrict: 'E',
template: '<div></div>',
replace: true,
link: function (scope, element, attrs) {
scope.$watch(function () { return attrs.chart; }, function () {
if (!attrs.chart) return;
var charts = JSON.parse(attrs.chart);
$(element[0]).highcharts(charts);
});
}
};
});
app.controller('Ctrl', function ($scope, $http, $timeout) {
$http.get('http://localhost:1234/abc/pqr').success(function (data, status) {
var score = [];
for (var i = 0; i < data.length; i++) {
score.push(data[i].Score);
}
var name = [];
for (var i = 0; i < data.length; i++) {
name.push(data[i].Name);
}
$scope.renderChart = {
chart: {
type: 'bar'
},
xAxis: {
categories: name
},
series: [{
data: score
}],
legend: {
enabled: false
}
};
}).error("error message");
$timeout($scope.fetch, 1000);
});