I got a very large list of about 200 items with text and images. ng-repeat is way to slow to render this smoothly. It tried it with this solution. Works nice. But not with collection-repeat.
My web-service return this:
There are events with specific dates. The events should be grouped by date. So in order to use collection repeat, how is it possible to insert dividers, if you cant use angular.filter groupBy?
I can offer you a partial solution which would only work if the dataset is ordered by the displayed field in the divider.
First of all we need to create a fake element in the array so that we can discriminate the divider amongst the other element.
Let's say we have a collection of posts fetched from a webservice:
.controller('mainController', function($scope, dataService) {
$scope.posts = [];
var divider = '';
});
the private field divider will be in use when we load the posts.
And we will have the loadMore method to load extra data when we scroll the list:
$scope.loadMore = function(argument) {
page++;
dataService.GetPosts(page, pageSize)
.then(function(result) {
if (result.data.length > 0) {
angular.forEach(result.data, function(value, key) {
value.divider = false;
if (value.postId !== divider)
{
divider = value.postId;
$scope.posts.push({divider: true, dividerText: value.postId});
}
$scope.posts.push(value);
});
}
else {
$scope.theEnd = true;
}
})
.finally(function() {
$scope.$broadcast("scroll.infiniteScrollComplete");
});
};
When we fetch the data from the web api (and the promise is resolved) we loop through the collection and check if the field is different from the divider. If this is a new divider we store the info and add a new element to the collection:
angular.forEach(result.data, function(value, key) {
value.divider = false;
if (value.postId !== divider)
{
divider = value.postId;
$scope.posts.push({divider: true, dividerText: value.postId});
}
$scope.posts.push(value);
});
As you can see I've added an element:
$scope.posts.push({divider: true, dividerText: value.postId});
I've used a dividerText field which will be displayed later on.
Now we need to create our own directive divider-collection-repeat which should be attached to a collection repeat:
<ion-item collection-repeat="post in posts" item-height="75" divider-collection-repeat>
I guess you're using infinite-scroll, so here is the whole HTML:
<ion-content ng-controller="mainController">
<ion-list>
<ion-item collection-repeat="post in posts" item-height="75" divider-collection-repeat>
{{post.name}}
</ion-item>
</ion-list>
<ion-infinite-scroll ng-if="!theEnd" on-infinite="loadMore()" distance="50%"></ion-infinite-scroll>
</ion-content>
this is the directive:
.directive('dividerCollectionRepeat', function($parse) {
return {
priority: 1001,
compile: compile
};
function compile (element, attr) {
var height = attr.itemHeight || '75';
var itemExpr = attr.collectionRepeat.split(' ').shift();
attr.$set('itemHeight', itemExpr + '.divider ? 40 : (' + height + ')');
attr.$set('ng-class', itemExpr + '.divider ? "item-divider" : ""');
var children = element.children().attr('ng-hide', itemExpr + '.divider');
element.prepend(
'<div ng-show="' + itemExpr + '.divider" class="my-divider" ' +
'ng-bind="' + itemExpr + '.dividerText" style="height:100%;">' +
'</div>'
);
return function postLink(scope, element, attr) {
scope.$watch(itemExpr + '.divider', function(divider) {
element.toggleClass('item-divider', !!divider);
});
};
}
});
The directive prepends an element (html) to the list using the expression you've defined in your collection-repeat.
In my sample I've use collection-repeat="post in posts" so this line:
var itemExpr = attr.collectionRepeat.split(' ').shift();
fetches the item's name; in my case it is going to be post.
We use the height as well cause we might need to have a different height for the divider.
This bit here is the place where all the magic happens:
element.prepend(
'<div ng-show="' + itemExpr + '.divider" class="my-divider" ' +
'ng-bind="' + itemExpr + '.dividerText" style="height:100%;">' +
'</div>'
);
It uses an ng-show for the field 'post.divider' (ng-show="' + itemExpr + '.divider") and binds the our text field ng-bind="' + itemExpr + '.dividerText"
I've also added a custom class my-divider just in case we need to change the layout of our divider a bit.
The final result is here or in this plunker.
As you might have noticed I haven't used a date field as I already had a sample where, sadly, I didn't have any dates.
I guess it should be very easy to adapt to your situation.
The directive is based on a sample I have found on github.
You will find the code for the directive here.
Related
I have to use AngularJS to build a dashboard and one of the components is a table.
Since I did not find relevant dependencies/libraries for angularjs (like tabulator or datatables), I am doing it myself.
Instead of using the native angular filter, I built a custom method, but I am not sure if I am following a good approach.
The main idea is that when I pull the data object (array of objects) via Ajax, I create both an "original" and a "current" data object,s and at the beginning, they are exactly the same of course.
Then I created an input field above every column heading and I linked the search function to the blur and keyup events (enter key).
When the search function is triggered, I start making changes to the "current" object. This way I can filter by multiple columns incrementally. I filter the data object using an awesome library called AlaSQL.
I also linked to a button the "reset" method, which simply makes the "current" object equal to the "original" object, and cleans up the input fields.
The point is, am I missing any best practices? Are there better ways to do so with AngularJS?
Any suggestions?
Thanks a lot.
HTML
<div ng-app="myApp">
<div ng-controller="divController">
<my-table></my-table>
</div>
</div>
JS
var app = angular.module('myApp', []);
app.controller('divController', function ($scope, $http) {
$scope.data = {};
$scope.data.current = null;
$scope.data.original = null;
$scope.filter = {
id: {
field: "id",
value: null
},
name: {
field: "name",
value: null
},
owner: {
field: "owner",
value: null
},
}
$scope.reset = function () {
console.log("reset");
$scope.data.current = $scope.data.original;
for (let prop in $scope.filter) {
$scope.filter[prop]["value"] = null;
}
}
$scope.filterExec = function (field, value) {
if (value) {
console.log(`Executing filter on field "${field.trim()}" by this value "${value.trim()}"`);
var filtered = alasql('SELECT * FROM ? where ' + field + ' LIKE "%' + value + '%"', [$scope.data.current]);
$scope.data.current = filtered;
}
}
$http.get("./workspaces_demo_obj.json")
.then(function (response) {
console.log(response);
$scope.data.original = response.data;
$scope.data.current = response.data;
});
});
app.directive('myTable', function () {
return {
template:
'<div>Total rows {{data.current.length}} <button ng-click="reset()">RESET</button></div>' +
'<table class="table table-responsive table-sm">' +
'<thead>' +
'<tr><th>Workspace ID</th>' +
'<th>Name</th>' +
'<th>Owner</th></tr>' +
'<tr><th><input ng-model="filter.id.value" ng-blur="filterExec(filter.id.field, filter.id.value)" ng-keydown="$event.keyCode === 13 && filterExec(filter.id.field, filter.id.value)" placeholder="Filter by id"></input></th>' +
'<th><input ng-model="filter.name.value" ng-blur="filterExec(filter.name.field, filter.name.value)" ng-keydown="$event.keyCode === 13 && filterExec(filter.name.field, filter.name.value)" placeholder="Filter by name"></input></th>' +
'<th><input ng-model="filter.owner.value" ng-blur="filterExec(filter.owner.field, filter.owner.value)" ng-keydown="$event.keyCode === 13 && filterExec(filter.owner.field, filter.owner.value)" placeholder="Filter by owner"></input></th></tr>' +
'</thead>' +
'<tbody>' +
'<tr ng-repeat="x in data.current">' +
'<td>{{ x.workspace_id }}</td>' +
'<td>{{ x.name }}</td>' +
'<td>{{ x.owner }}</td>' +
'</tr>' +
'</tbody>' +
' </table>',
restrict: 'E'
};
});
I am showing data to user using angular ui-grid. In the following code for field 'plan', i add a set of entities by calling the getPlan() function and show it using a cellTemplate in ui-grid columnDefs.
$scope.getPlan = function(rowEntity) {
var plan1Temp = rowEntity.plan1;
var plan2Temp = rowEntity.plan2;
var plan3Temp = rowEntity.plan3;
var plan4Temp = rowEntity.plan4;
var plan5Temp = rowEntity.plan5;
var plan6Temp = rowEntity.plan6;
var plan7Temp = rowEntity.plan7;
var plan8Temp = rowEntity.plan8;
plan = plan1Temp+plan2Temp+plan3Temp+plan4Temp+plan5Temp+plan6Temp+plan7Temp+plan8Temp;
console.log("plan=", plan);
return plan;
};
columnDefs: [{ field : 'plan',
cellTemplate : '<div id="grid-cell-template">
{{grid.appScope.getPlan(row.entity)}}</div>',
displayName : "Plan",
footerCellTemplate : '<div class="ui-grid-cell-contents">
\Total:\ {{grid.appScope.getTotalPlan() }}</div>',
width : "*"
}]
Then to show the total of all the plans i use footerCellTemplate with getTotalPlan(). While using it i got $digest() iterations reached error. I noticed the getPlan() function is called many times. So i added $watch like shown below.
var totalPlan = [];
$scope.$watch(function() {
return plan;
}, function() {
totalPlan.push(plan);
console.log("totalPlan =", totalPlan);
});
$scope.getTotalPlan = function() {
var sum = 0;
angular.forEach(totalPlan ,function(item){
sum = sum + item;
});
return sum;
console.log("getTotalPlan=", sum);
};
After adding $watch also the getPlan() function is called many times. Also in totalPlan[] it holds only 2 values, first null and second one last record value(in total there are 3 records) and prints this value in the ui-grid footercelltemplate. How should i use $watch to make it work right?
So basically the problem is i am not able to insert 'plan' values into totalPlan[ ] and show the total in the grid footer template.
Also i tried using :: inside the expression, the number of getPlan() calling times reduced to half but no luck.
I am using a ContextMenu directive within a kendo grid. I have made one change to it so I can include icons in the text (changed $a.text(text) to $a.html(text).
I have one in the first cell (I highjacked the hierarchical cell) that has row operations (add, clone& delete) and one on a span within each cell that changes the cell values operation (addition, subtraction, equals, etc...)
Both of these were working. I am unsure what I changed that stopped it from working because I last checked it several changes ago (I'm still locked out of TFS so I can't revert).
One change I made was to include a disabled/enabled check to the working contextMenu. I tried adding the same to the broken one and no dice.
I do perform a $compile on the working menu and the broken one is only included in the kendo field template.
If I must compile the field template (and I didn't need to before), how can this be done?
So here is some code.
working menu:
$scope.getRowContextMenu = function (event) {
var options =
[[
"<span class='fa fa-files-o'></span>Clone Rule", function (scope, cmEvent) {/*omitted for brevity*/}),rowContextDisableFunction]]
}
var setHierarchyCell = function (grid) {
var element = grid.element;
var hCells = element.find("td.k-hierarchy-cell");
hCells.empty();
var spanStr = "<span context-menu='getRowContextMenu()' class='fa fa-bars'></span>";
hCells.append($compile(spanStr)($scope));
var span = hCells.find("span.fa");
span.on('click', function (event) {
$(this).trigger('contextmenu', event);
});
}
kendo template:
var mutliFormTemplate = function (fieldName, type) {
var result = "";
result += "<span context-menu='getOperationContextMenuItems()' class='fa #= " + fieldName + "_Obj.OperationSymbol # type-" + type + "'> </span>\n";
/*The rest pertains to the cell value. excluded for brevity*/
return result;
}
$scope.getOperationContextMenuItems = function () {
//I trimmed this all the way down to see if I could get it working. Still no joy
return [
["test", function () { }, true]
];
}
Creating the kendo columns dynamically:
$scope.model = {
id: "RuleId",
fields: {}
};
$scope.fieldsLoaded = function (data, fields) {
var column = {}
$.each(fields, function () {
var field = this;
$scope.columns.push({
field: field.Name,
title: field.Name,
template: mutliFormTemplate(field.Name, "selector")
});
column[field.Name ] = { type: getFieldType(field.Type.BaseTypeId) }
});
$scope.model.fields = column;
}
Thanks for any and all help ^_^
I need to have a column in ag-grid where i can select multiple values from dropdown. I just googled online to see if it is already implemented but i could find only one link.
https://gist.github.com/gaborsomogyi/00f46f3c0ee989b73c92
Can someone let me know how to implement it. show the full code as an example please.
Here is the code shared over there.
function agDropDownEditor(params, optionsName, optionsList) {
_.set(params.$scope, optionsName+'.optionsList', optionsList);
var html = '<span style="width:100%; display:inline-block" ng-show="!'+optionsName+'.editing" ng-click="'+optionsName+'.startEditing()">{{data.'+params.colDef.field+'}}</span> ' +
'<select style="width:100%" ng-blur="'+optionsName+'.editing=false" ng-change="'+optionsName+'.editing=false" ng-show="'+optionsName+'.editing" ng-options="item for item in '+optionsName+'.optionsList" ng-model="data.'+params.colDef.field+'">';
// we could return the html as a string, however we want to add a 'onfocus' listener, which is not possible in AngularJS
var domElement = document.createElement("span");
domElement.innerHTML = html;
_.set(params.$scope, optionsName+'.startEditing', function() {
_.set(params.$scope, optionsName+'.editing', true); // set to true, to show dropdown
// put this into $timeout, so it happens AFTER the digest cycle,
// otherwise the item we are trying to focus is not visible
$timeout(function () {
var select = domElement.querySelector('select');
select.focus();
}, 0);
});
return domElement;
}
Hope this helps, this is just a snippet of my code what i'm doing is I'm fetching from an array using map and then creating my object which is col and returning it and this will repeat till the last index of that array.
var col = {};
col.field = "fieldName";
col.headerName = "colName";
col.headerCellTemplate = function() {
var eCell = document.createElement('span');
eCell.field = obj.expr;
eCell.headerName = obj.colName;
eCell.innerHTML = "<select>"+"<option>"+
'Abc'+"</option>" +"<option>"+
'Xyz'+"</option>" +"</select>"
//$scope.dropDownTemplate;
var eselect = eCell.querySelector('select');
eselect.focus();
return eCell;
};
return col ;
}));
From what I have seen with Angular 2.0, I have a feeling I am going to be using Angular 1.x for a while. It has all the building blocks that I think I need, the only downside is that it does have performance issue with dirty checking so I am trying to think about that more. Now ng-repeat can be an issue because of the number of watchers it adds.
So I have this part of a template (in jade):
li(ng-repeat='topMenuItem in sideMenu.topMenu', ng-class='{"is-active": topMenuItem.display === sideMenu.activeTopMenu}')
a(href='{{ topMenuItem.href }}') {{ topMenuItem.display }}
ul(ng-if='sideMenu.secondMenu.length > 0 && topMenuItem.display === sideMenu.activeTopMenu')
li(ng-repeat='secondMenuItem in sideMenu.secondMenu', ng-class='{"is-active": secondMenuItem.display === sideMenu.activeSecondMenu}')
a(href='{{ secondMenuItem.href }}') {{ secondMenuItem.display }}
When this displays 22 menu items the number of watchers is 90 (using this bookmark snippet).
I decided to play around with trying to use $interpolate to generate that menu. I ended up with a directive with this for the compile function:
compile: function(element, attributes) {
var topLevelExpression = $interpolate('<li{{ cssClass }}>{{ topMenuItem.display }}{{ secondLevelMenuHtml }}</li>');
var secondLevelExpression = $interpolate('<li{{ cssClass }}>{{ secondMenuItem.display }}</li>');
var updateMenu = function() {
var html = '';
sideMenu.topMenu.forEach(function(topMenuItem) {
var cssClass = topMenuItem.display === sideMenu.activeTopMenu ? ' class="is-active"': '';
var secondLevelMenuHtml = '';
if(sideMenu.secondMenu.length > 0 && topMenuItem.display === sideMenu.activeTopMenu) {
secondLevelMenuHtml += '<ul>';
sideMenu.secondMenu.forEach(function(secondMenuItem) {
var cssClass = secondMenuItem.display === sideMenu.activeSecondMenu ? ' class="is-active"': '';
secondLevelMenuHtml += secondLevelExpression({
secondMenuItem: secondMenuItem,
cssClass: cssClass,
});
});
secondLevelMenuHtml += '</ul>';
}
html += topLevelExpression({
topMenuItem: topMenuItem,
cssClass: cssClass,
secondLevelMenuHtml: secondLevelMenuHtml
});
});
element.find('.application-navigation').html(html);
};
return function($scope) {
$scope.$watchCollection('sideMenu', function() {
updateMenu();
});
}
}
From my testing, this code functions exactly the same as the ng-repeat, the output look as it should. This version only has 16 watchers and that number does not increase when more elements are shown where the ng-repeat does.
Since this code is only doing the bare minimum that is needed for this piece of code to work, I imagine the javascript itself is just as efficient (if not more efficient) than the code that executes for ng-repeat.
Is that assumption correct?
Are there any issues with doing looping DOM generation in this way vs using ng-repeat?
Tricky question.
I initially thought that if you one-time-bind the property of an object, then if the object changed, the properties would be re-bound.
<div>{{::menu.menuItem}}</div>
<button ng-click="changeMenu()">switch</div>
That is NOT the case.
What one could do though - albeit with a bit of a flaky/hacky approach (perhaps a better approach could be suggested in comments) - is to force Angular to re-bind. One such candidate is ng-if:
<div ng-if="bound">
<div>{{::menu.menuItem1}}</div>
<div>{{::menu.menuItem2}}</div>
</div>
<button ng-click="changeMenu()">Switch</div>
Then, in the controller:
$scope.bound = true;
$scope.menu = {..}; // menu 1
$scope.changeMenu = function(){
$scope.menu = {..}; // menu 2
$scope.bound = false;
$timeout(function(){$scope.bound = true;}, 0);
}
Plunker