AngularJs. Ng repeat jquery sortable unexpected behavior - angularjs

I faced with unexpected behavior while using jquery sortable in conjunction with angular ng repeat directive. I'm trying to sort items inside different columns in a table.
When I'm dragging 1st element of 2nd column to 1st position (or to 2nd position) in 1st column - all is OK, but if I'll try to put 1st element of 2nd column to 3rd or 4th position it just goes away with subsequent elements.
If you would look at json encoded array at the bottom of document you will see that components are correctly sorted.
I would be grateful for any kind of assistance.
Here is live example: http://plnkr.co/edit/j6xMb4vhcUDVk8POjzpW?p=preview
HTML
<div data-ng-app="myApp" data-ng-controller="defaultCtrl">
<table border="1" data-dnd-list="doc">
<tr>
<td data-ng-repeat="column in doc.columns" data-index="{{$index}}" class="column">
<table class="item" style="border:1px solid red; cursor: pointer" width="100%" data-ng-repeat="component in column.components" data-index="{{$index}}">
<tr>
<td style="padding:5px">{{ component.content }}</td>
</tr>
</table>
</td>
</tr>
</table>
{{ doc }}
</div>
JS
var app = angular.module("myApp", []);
app.controller("defaultCtrl", ["$scope", function($scope) {
$scope.doc = {
columns: [
{
components: [
{
content: "1 column 1 component"
},
{
content: "1 column 2 component"
},
{
content: "1 column 3 component"
}
]
},
{
components: [
{
content: "2 column 1 component"
}
]
}
]
}
}]);
app.directive('dndList', function() {
return function(scope, element, attrs) {
var toUpdate;
var oldComponentIndex = -1;
var oldColumnIndex = -1;
scope.$watch(attrs.dndList, function(value) {
toUpdate = value;
console.log("New changes: ", toUpdate);
},true);
$(element[0]).sortable({
items:'.item',
start:function (event, ui) {
var i = $(ui.item);
// on start we define where the item is dragged from
oldComponentIndex = i.attr("data-index");
oldColumnIndex = i.closest(".column").attr("data-index");
},
stop:function (event, ui) {
var i = $(ui.item);
var newComponentIndex = i.index();
var newColumnIndex = i.closest(".column").attr("data-index");
var toMove = toUpdate.columns[oldColumnIndex].components[oldComponentIndex];
toUpdate.columns[oldColumnIndex].components.splice(oldComponentIndex, 1);
toUpdate.columns[newColumnIndex].components.splice(newComponentIndex, 0, toMove);
scope.$apply(scope.doc);
}
})
}
});

OK. I found a solution. The fact is that when you put new item in sortable list, the DOM became unsynchronized with AngularJS. To prevent this you need to remove HTML of new item.
i.remove();
Here is updated plunker. http://plnkr.co/edit/j6xMb4vhcUDVk8POjzpW?p=preview

Related

DataTables inside bootstrap accordion in angularjs

I am working on a module where I have to redesign some products. Following is the screenshot of how the products used to get displayed previously.
Now the products will be displayed inside accordion of their specific name. I am bound to use ui.bootstrap Version: 0.11.0 - 2014-05-01. Following is a sketch of how the products will be displayed now. In each accordion there will be a datatable of that particular product in which the columns will dynamically generate and we would be able to check the particular products we want.
Following is my html code:
<accordion>
<accordion-group ng-repeat="AllProduct in AllProducts">
<accordion-heading>
{{AllProduct.TypeName}}
</accordion-heading>
</accordion-group>
<table id="dtVoice" class="table manage-user-table offer-mgt-table" dt-options="dtOptions" dt-columns="dtColumns"></table>
</accordion>
The way i have dynamically created datatables are as follows:
dtColumns.push(DTColumnBuilder.newColumn(null).withTitle('').notSortable()
.renderWith(function(data, type, full, meta) {
return '<input type="checkbox" ng-model="showCase.selected[' + data.id + ']"/>';
}));
for (var key in $scope.VoiceProducts[0]) {
if (key == "ProductName" || key == "LongDistanceMinutes" || key == "IsCallWaiting") {
dtColumns.push(
DTColumnBuilder.newColumn(key).withTitle(key)
)
}
$scope.dtColumns = dtColumns;
$scope.dtOptions = DTOptionsBuilder.newOptions()
.withOption('data', $scope.VoiceProducts)
.withOption('dataSrc', '')
angular.element('#dtVoice').attr('datatable', '')
}
$compile(angular.element('#dtVoice'))($scope);
Following is my json
[
{
"ProductList": [
{
"ProductName": "Voice",
"IsActive": false,
"IsDeleted": false,
"LongDistanceMinutes": "",
"IsCallWaiting": "",
"CallWaitingId": "",
"IsThreeWayCalling": "",
"IsCallerId": "",
"IsCallForwarding": "",
"IsCallRejection": "",
"ID": 552,
"OfferId": 0
}
],
"ID": 2,
"IsActive": false,
"IsDeleted": false,
"TypeName": "Voice"
}
]
How to put this datatable inside accordion? Because by doing whatever I'm, i'm unable to achieve it.
I had the same problem on the pass.
Please use ng-if as a flag to recreate the table when the accordion item is active. The accordion and tab components avoid the table to be shown.
The code below can help.Notice the ng-click and ng-if
<accordion>
<accordion-group ng-repeat="AllProduct in AllProducts">
<accordion-heading ng-click="setGroup('AllProduct.TypeName')">
{{AllProduct.TypeName}}
</accordion-heading>
<table id="dtVoice" ng-if="group=='AllProduct.TypeName'" class="table manage-user-table offer-mgt-table" dt-options="dtOptions" dt-columns="dtColumns"></table>
</accordion-group>
</accordion>
UPDATED: As per new information (using angular-datatable)
The solution now boils down to computing the columns and options per accordion-group.
Working Plunker with 2 accordion groups
As you can see in the HTML below the options and columns are computed per accordion.
<table datatable class="table manage-user-table offer-mgt-table" dt-options="getDtOptions(AllProduct)" dt-columns="getDtColumns(AllProduct)"></table>
Angular code showing getDtColumns and getDtOptions. I have kept the data very simple for demonstration purposes and copied the current dtColumns code however you can customize it so that you can even have more than 1 type of table :
var app = angular.module('myApp', ['ui.bootstrap', 'datatables']);
app.controller('myCtrl', function($scope, DTColumnBuilder, DTOptionsBuilder, DTColumnDefBuilder, $timeout, AllProducts) {
$scope.AllProducts = AllProducts
$scope.getDtColumns = function(allProduct) {
var items = allProduct.ProductList;
if (allProduct.dtColumns) allProduct.dtColumns.length = 0;
allProduct.dtColumns = allProduct.dtColumns || [];
var dtColumns = allProduct.dtColumns;
dtColumns.push(DTColumnBuilder.newColumn('').withTitle('').notSortable()
.renderWith(function(data, type, full, meta) {
return '<input type="checkbox" ng-model="showCase.selected[' + full.id + ']"/>';
}));
for (var key in items[0]) {
if (key == "ProductName" || key == "LongDistanceMinutes" || key == "IsCallWaiting") {
dtColumns.push(
DTColumnBuilder.newColumn(key).withTitle(key).notSortable()
)
}
}
return dtColumns;
};
$scope.getDtOptions = function(allProduct) {
if (allProduct.options) return allProduct.options;
var items = allProduct.ProductList;
allProduct.options = allProduct.options || DTOptionsBuilder.newOptions().withOption('aaData', items);
return allProduct.options;
};
});
OLDER ANSWER without angular-datatable
First of all I do not recommend jQuery DataTable or any other jQuery component in AngularJS applications. I personally try not to bundle jQuery or perform DOM manipulation using jQuery.
However to get you going with what you have I suggest the following:-
Remove these two lines because simply adding those attributes datatable dynamically is not going to trigger the DataTable binding:-
angular.element('#dtVoice').attr('datatable', '')
}
$compile(angular.element('#dtVoice'))($scope);
and try using something like this:-
$('#dtVoice').DataTable( {columnDefs: $scope.dtColumns });
Further more just to clean up a bit I create a new directive (just typing out loud):
app.directive('myDatatable`, function(){
return {
restrict: 'A',
scope: {
'dtColumns': '='
}
link: function($scope, elem, attr) {
$('#dtVoice').DataTable( {columnDefs: $scope.dtColumns});
}
};
});
and your table something like below:
<table id="dtVoice"
class="table manage-user-table offer-mgt-table"
dt-options="dtOptions"
dt-columns="dtColumns" my-datatable></table>

Delay in ng-repeat

How can I insert delay between every ng-repeat iterations so my table will generate records slower. Is there any way to do it without using ngAnimate.
<table>
<tr ng-repeat="x in records">
<td>{{x}}</td>
</tr>
</table>
[Suggestion]
If you data is loading slow, maybe is because you have duped keys, so for test it you can try with track by $index like this
<table>
<tr ng-repeat="x in records track by $index">
<td>{{x}}</td>
</tr>
</table>
[Solution ]
If you still want to control the interaction of ng-repeat, it is best to create a dynamic variable that is manipulated as time passes, then you can have a primary array with all records
$scope.records = [
{
"name": "name1",
"data2": "data2.1",
"data3": "data3.1"
},
{
"name": "name2",
"data2": "data2.2",
"data3": "data3.2"
},
{
"name": "name3",
"data2": "data3.3",
"data3": "data3.3"
}
];
Then you could use setTimeout to call a function that passes data from the primary array to another final array, an index per interaction
//start to proccess
setTimeout(function(){$scope.Result();},1000);
//Here pass data from Records to FinalResult in each interaction
$scope.Result=function(){
dif=$scope.records.length-$scope.FinalResult.length;
currentRow=$scope.FinalResult.length;
if(dif>0){
$scope.FinalResult.push($scope.records[currentRow]);
}
if($scope.records.length>$scope.FinalResult.length){
setTimeout(function(){$scope.Result();},1000);
}else{
console.log('Finish Load');
$scope.FinishRender=true;
}
//refresh
$scope.$apply();
}
And finally deliver this variable with another function...
//get the finish Array
$scope.getFinalResult=function(){
return $scope.FinalResult;
}
and HTML
<body>
<div ng-controller="recordsCtrl">
<table style="border:1px solid black">
<tr ng-repeat="x in getFinalResult()">
<td>{{x.name}}</td>
<td>{{x.data2}}</td>
<td>{{x.data3}}</td>
</tr>
</table>
<div ng-if="FinishRender" style="color:red;font-weight:bold">Data Loaded!!!</div>
</div>
</body>
Please feel free to check a solution in punkler
[Optional]
Also you could use a directive to control the last interaction like this
myApp.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
console.log(element);
if (scope.$last === true) {
console.log('Finish Load');
}
}
}
});
and html
<table>
<tr ng-repeat="x in getFinalResult()" on-finish-render="onFinishRender">
....
...
</tr>
</table>
note:I'm not really sure but I think it's possible to capture every interaction with this method
A possible solution in your situation could be to take the source array and populate the ng-repeat array in increments with a delay using _.chunk and $timeout, as such:
index.html
<table>
<tr ng-repeat="x in records track by $index">
<td>{{x}}</td>
</tr>
</table>
appCtrl.js
$scope.sourceData = [data, data, data];
$scope.records = [];
/**
*#param source (array): the array with the data used to populate the ng-repeat array
*#param target (array): the array to which ng-repeat points
*#param delay (integer): the render delay, in milliseconds
*#param renderSize (integer): the amount of list items to render between each delay
*
**/
function delayedRender(source, target, delay, renderSize) {
var promise = $q.resolve();
function scheduleRender(partial) {
Array.prototype.push.apply(target, partial);
// the timeout will ensure that your next render won't occur before the delay
return $timeout(function(){}, delay);
}
// _.chunk is a Lodash function that takes an array and chops it into smaller chunks.
// 'renderSize' is the size of these chunks.
var partials = _.chunk(source, renderSize);
var next;
// here we schedule renders to occur only after
// the previous render is finished through the use of $q promises
_.forEach(partials, function(partial) {
next = scheduleRender.bind(null, partial);
promise = promise.then(next);
});
}

Bind data from pagination to filter in Angular

I have a simple pagination with letters:
<div ng-controller="DataController">
<ul class="pagination">
<li ng-repeat="letter in data_letters">
{{letter}}
</li>
</ul>
<table class="table table-striped">
<tbody>
<tr>
<th>Name</th>
<th>State</th>
</tr>
<tr ng-repeat="person in persons | startsWithLetter:letter">
<td>{{person.Name}}</td>
<td>{{person.State}}</td>
</tr>
</tbody>
</table>
</div>
What is the easiest way to bind the letter (on which we clicked at the paginaton) to the table filter.
Here is a fully functional plunkr: http://plnkr.co/edit/Trr5LzrcMfZqonD0jvjX?p=preview
I have everything implemented already. It is just the data-binding which is missing. Any ideas?
One issue with your code is that you are unnecessarily injecting $scope into your filter:
app.filter('startsWithLetter', function($scope) {
should be
app.filter('startsWithLetter', function() {
The other issue is using 'C-D' as a filter value. I would change $scope.data_letters to be an array of objects that contain a key/value pair like this:
$scope.data_letters = [{
key: 'A',
value: "A"
}, {
key: "B",
value: "B"
}, {
key: ['C', 'D'],
value: "C-D"
}];
Then change your check in the filter to evaluate all keys, like this:
angular.forEach(letters, function(letter, key) {
if (itemName.startsWith(letter)) {
filtered.push(items[i]);
}
});
You can see it working here where I forked your plunk.
Updated your plunk here
Problem was that your filter was not recognized by angular.
Also, I would suggest using
Chrome Developer Tools
it shows useful debug information
app.filter('startsWithLetter', function() {
console.log("Inside startsWithLetter");
return function(items, letter) {
console.log("Inside Filter: Starts with " + letter);
var filtered = [];
for (var i = 0; i < items.length; i++) {
var itemName = items[i].Name;
if (itemName.startsWith(letter)) {
filtered.push(items[i]);
}
}
return filtered;
};
});

Angular print service timeout issue for multiple print

var userServicePromise = UserService.printBarCodes(sampleId);
userServicePromise.then(function(response) {
if (response != null && response.data != null && response.data.result != null) {
response.data.result.forEach(function(entry) { //three values in the array, iteraring three times.
$timeout(function() {
vm.barCodeImage = angular.copy(entry);
$timeout(function() {
PrintService.printElement("printThisElement"); // display one value three times
}, 1);
}, 2);
});
} else {
toaster.error(response.data.message);
}
});
using print service to print the div, values of the div are populated using the variable(vm.barCodeImage).
But here one value is displaying all the time in the print. tried adding time out interval after the for each, but no luck.
//print service code
(function() {
'use strict';
angular.module('app.services')
.factory('PrintService', PrintService);
PrintService.$inject = [];
function PrintService() {
var service = {
printElement: printElement
};
return service;
function printElement(elem) {
var printSection = document.getElementById('printSection');
// if there is no printing section, create one
if (!printSection) {
printSection = document.createElement('div');
printSection.id = 'printSection';
document.body.appendChild(printSection);
}
var elemToPrint = document.getElementById(elem);
// clones the element you want to print
var domClone = elemToPrint.cloneNode(true);
printSection.innerHTML = '';
printSection.appendChild(domClone);
window.print();
window.onafterprint = function() {
printSection.innerHTML = '';
}
};
}
})();
The expected result for this should be three different print(suppose loop size is 3, it should be 7858,7859,7860), but here it displays all same.
printThisElement id is an HTML.
<div id="printThisElement" class="onlyprint" >
<table>
<tr>
<td>{{ ctrl.instCode }}</td>
<td align="center">{{ ctrl.date | dateDisplayFilter}} </td>
</tr>
<tr>
<td colspan="2" align="center"> <img ng-src="data:image/JPEG;base64,{{ctrl.barCodeImage}}"> </td>
</tr>
<tr>
<td colspan="2" align="center">{{ ctrl.user.name }} </td>
</tr>
<tr>
<td >Reg Id: {{ ctrl.regIdLookup }}</td>
<td align="center">{{ ctrl.testName }}</td>
</tr>
</table>
</div>
Ok, I supposed your binding is wrong. printThisElement refer to single data in controller. So if you have multiple entries, it must be an array.
I suggest to you to use a directive instead of a service to print element in DOM.
I create a little plunker to illustrate it : https://plnkr.co/edit/QvmqV88wZCYbsehMWh3W?p=preview
This plunker show how from an array resulting of calling an async service you can display all entries.
The directive is very simple :
.directive('print', function() {
return {
restrict: 'E',
scope: {
data: '='
},
template: '<div id="printThisElement" class="onlyprint" ><table><tr> <td>{{ data.instCode }}</td></tr><tr><td>... Other values in data ...</td></tr></table></div>',
}
})
The template of the directive is your printThisElement id. With this way you can display multiple result isolate by directive.
Hope this will help you
Edit : Solution for the problem was report on this stack Inconsistent Data on multiple page printing on a printer

Multiple ng-repeat on single element

Is this possible to achieve a code like this:-
<tr ng-repeat="data in dataArray,value in valueArray">
{{data}} {{value}}
</tr>
I am having two arrays I want to show them in single row.
PS: I am not asking for syntax. I am looking for logic to achieve this
Thanks
Like :- "http://jsfiddle.net/6ob5bkcx/1/"
You should be doing this in the controller, not in the view. Map the dataValues into a key/value pair object and reference the values array using an index. This assumes that each data key has a corresponding value key.
Controller:
var dataArray = [];
var valueArray = [];
this.repeatData = dataArray.map(function(value, index) {
return {
data: value,
value: valueArray[index]
}
});
View:
<tr ng-repeat="data in repeatData">
{{data.data}} {{data.value}}
</tr>
Does this suits your need
http://jsfiddle.net/jmo65wyn/
Your data, value array as object array
this.obj = [{data: 'a', value :true}, {data: 'b', value:true}];
And you loop like this
<div ng:repeat="o in obj">
{{o.data}} and {{o.value}}
<input type="checkbox" ng:model="o.value">
</div>
Angular ng-repeat does not support it but still you can write your own custom directive according to your requirements.
Update Section
var traverseCollection = angular.module("CollectionTraverse", []);
traverseCollection.directive("repeatCollection", function(){
return {
restrict: "A",
scope: {
model: "="
},
controller: controller: function($scope) {
var collectionList = $scope.model;
angular.forEach(collectionList, function(obj, index) {
angular.forEach(obj, function(data, index) {
});
});
}
}
});
Your scope should contains the list of your collection objects : $scope.collectionList = [dataArray, valueArray];
Usage in HTML:
-------------------------------------------
<div repeat_collection model="collectionList"></div>
This directive will be generic to traverse list of collections and yes in the above code there can be some syntactical errors because i did not run it. Its your luck.
If you have, for any reason, two arrays with the same length and where their contents are corresponding (array1[0] correspond to array2[0], ..., array1[n] correspond to array2[n]), you can use AngularJS's track by (which was introduced for the 1st time in the version 1.1.4) like this for example :
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
<table ng-app="" ng-init="names=['Bill','Billa','Billy']; ages=['10', '20', '30']">
<tr ng-repeat="name in names track by $index">
<td>{{ name }} is {{ ages[$index] }} years old.</td>
</tr>
</table>
Hope that can help.
if you want something like a list with two or more items in the same row:
in html file:
<li ng-repeat="item in items">
<i class="material-icons">{{navItem[1]}}</i>{{navItem[0]}}</li>
in js file:
$scope.items = [
["Home", "home"],
["Favorite", "favorite"]
]

Resources