AngularJS - $index not updated with keyPress - angularjs

I'm pretty new in AngularJS so this might be easy to solve,
I have a list of meetings I'm trying to navigate between them with my keyboard using keyPress,
The problem is that after the first navigation (UP or DOWN) using the keyboard, the $index is not updating, so for instance if I'm on meeting number 4, and clicking on the "UP" key, I will navigate to meeting 3, but clicking 'DOWN' afterwards will navigate me to meeting number 5 (and not back to 4, becuase my $index is stuck at 4)
the HTML looks like this:
<ul class="meeting-list">
<div ng-repeat="meeting in meetings" ng-init="meeting.innerIndex = $index">
<div class="meeting-block" ng-keydown="KeyPress($event, $index)" ng-click="showMeeting(meeting)" >
<span class="title">{{meeting.Title}}</span><br/>
</div>
</div>
</ul>
This is my JS code:
$scope.meetings = [{"title":"sub1","id":123},{"id":124,"title":"sub2"},{"title":"sub3","id":125},{"title":"sub4","id":126}];
$scope.KeyPress = function(keypress, index) {
if (keypress.which == 38) { //UP
if (index != 0) {
for (var i = 0; i < $scope.meetings.length; i++) {
if ($scope.meetings[i].innerIndex == (index - 1)){
$scope.showMeeting($scope.meetings[i]);
return ;
}
}
}
if (keypress.which = 40) { //DOWN
if ($scope.meetings.length - 1 != index) {
for (var i = 0; i < $scope.meetings.length; i++) {
if ($scope.meetings[i].innerIndex == (index + 1)) {
$scope.showMeeting($scope.meetings[i]);
return ;
}
}
}
};
$scope.showMeeting = function(meeting) {
$location.path('meetings/view/' + meeting.Id);
};

$index is a property of an ng-repeated item, it is always equal to the position in the ng-reapeated array. I am not sure if it's implemented to be unassignable, but I expect it should be and you should treat it that way.
Instead of trying to change the index, you should have your own property that keeps track of the index in the array that you're selected on.
This is trivial to do, just initialise it in your controller, then use it instead of your $index when navigating.
$scope.selectedIndex = 0; //start with the first meeting selected.

You have 2 problems here.
You cannot use the $index to navigate up and down the list
When you do index -= 1; this as no effect whatsoever on the $index or the selected meeting. You are passing the $index's value (an immutable number) and not it's reference. So, if you have $index = 5 and do index -= 1 (on the KeyPress), you will end up with: $index = 5 and index = 4.
You need to change the focus of the selected meeting in order to update the element that will receive the keyboard event
In order to change the focus you could use something like this:
// UP
$(keypress.target).prev().trigger('focus');
...
// DOWN
$(keypress.target).next().trigger('focus');
But I'm not sure if it's a 100% applicable to your case. You may need to define another way to select the element that will receive the focus. A directive here would be a good idea...

Related

calling function nested within ng-repeat not returning correct value to $scope

I'm trying to count the number of trivia questions a user has answered. For each questio (statement), where the value is greater than 0 -- that indicates the question is answered. I can see these correct values in the console.log, but I cannot get the value to show in $scope or displayed on the frontend: It displays a number, but the number does not match the results of the console.log:
html / haml :
categories(ng-repeat='category in service.categories' ng-class="{'first':$first}" )
.row(ng-click='showQuestions = !showQuestions' ng-class="{'ballotSelected' : showQuestions}")
.label
.chevron(ng-class="showQuestions ? 'fa fa-chevron-down ballotSelected' : 'fa fa-chevron-right'")
.name.text-uppercase
{{category.name}} {{category.statements.length}}
.questionCount(ng-init='getStatementCount(category)')
{{statementCounts}}
.showRow(ng-show='showQuestions')
controller:
$scope.getStatementCount = function(category) {
var i, questionCount;
console.log(category);
i = 0;
questionCount = 0;
while (i < category.statements.length) {
if (category.statements[i].value > 0) {
questionCount += 1;
}
i++;
}
console.log(questionCount, 'COUNT');
return $scope.statementCounts = questionCount;
};
Eevery time you call getStatementsCount(), you overwrite a unique scope variable statementCounts, that is displayed for all the categories.
That can't be right.
So,
either you store the result in the category itself, and display the category's result in each category. You should do that without using ng-init, in the controller, as soon as the categories are loaded/provided.
or you don't assign any variable with the result, and display the result directly in the page:
.
$scope.getStatementCount = function(category) {
...
return questionCount;
}
and
.questionCount
{{ getStatementCounts(category) }}

AngularJS dirPagination Select All Visible Rows

I'm building a CRUD data management project using Angular and the dirPagination directive and need to have a "select all" checkbox that selects all rows that are visible /without/ using jQuery.
I started with this - note - I am climbing the Angular learning curve and am aware that some/all of this may not be "the Angular way" but I also don't want to start fiddling (no pun) with the guts of dirPagination, ergo the dirPagination directive:
<tr dir-paginate-start="item in items|filter:filterFunction()|orderBy:sortPredicate:reverse| itemsPerPage: rowsPerPage">
and the individual row-checkbox
<input type="checkbox" class="rowSelector" ng-model="item.isSelected" ng-change="rowSelect($index, $event)"/> status: {{item.isSelected}}
and the related model elements:
$scope.items = [] //subsequently filled
$scope.rowsPerPage = 5;
$scope.rowSelect = function (ix, $event) {
var checked = (typeof $event == 'undefined') ? false : true;
if (!checked) { $scope.masterCheck = false; }
var rpp = $scope.rowsPerPage;
var p = $scope.__default__currentPage; //dirPagination's current page
var start = ((Math.max(0, p - 1) * rpp));
$scope.items[start + ix].isSelected = checked;
};
that works as expected. Check/uncheck a row and the {{item.isSelected}} value is updated in the model and is displayed beside the checkbox.
Then I added this /outside/ of the dirPagination repeat block:
<input type="checkbox" id="masterCheckbox" ng-model="masterCheck" ng-click="checkAll()" />
and the related function in the model:
$scope.masterCheck = false;
$scope.checkAll = function () {
var rpp = $scope.rowsPerPage;
var p = $scope.__default__currentPage; //dirPagination's current page
var start = ((Math.max(0, p - 1) * rpp));
var checked = $scope.masterCheck == true;
var rows = document.getElementsByClassName("rowSelector");
for (var ix = 0; ix < rows.length; ix++) {
rows[ix].checked = checked;
$scope.items[start + ix].isSelected = checked;
}
}
however in the checkAll() function checking/unchecking the individual rows isn't reflected in the {{item.isSelected}} display of each row.
Explicitly setting the individual item with
$scope.items[start + ix].isSelected = checked;
seems to set the 'isSelected' property of that item within the scope of the checkAll function but the row display does not change.
Clearly I have something wrong perhaps misunderstanding a Scope issue but at this point I'm stumped.
Any help greatly appreciated :-)
The light dawned, finally.
checkAll() as written tried to access each row by calculating its position using dir-paginate's __default__currentPage and Angular's row $index.
Of course that doesn't work because the items[] collection held by dir-paginate has been subjected to filtering and sorting, so while items[] do get checked (item.isSelected = true) the selected items/rows were living on non-visible pages. i.e. - we were selecting the wrong indexes.
One solution is comprised of the following -
The master checkbox
<input type="checkbox" id="masterCheckbox" ng-model="masterCheck" ng-click="checkAll()" />
The row checkbox (note function calls)
<input type="checkbox" class="rowSelector" value="{{sourceIndex('senId',item.senId)}}" ng-model="item.isSelected" ng-click="rowSelect(this)" />
the dir-paginate directive controls tag
<dir-pagination-controls on-page-change="onPageChange(newPageNumber)" max-size="15" direction-links="true" boundary-links="true" pagination-id="" template-url=""></dir-pagination-controls>
and the related $scope values and functions:
$scope.items = [];
$scope.masterCheck = false;
$scope.onPageChange = function (newPageNumber) {
//clear all selections on page change
//dir-paginate provides this hook
$scope.masterCheck = false;
for (var i in $scope.items) {
$scope.items[i].isSelected = false;
}
}
$scope.rowSelect = function (item) {
//if one is unchecked have to turn master off
if (!item.isSelected) $scope.masterCheck = false;
}
$scope.sourceIndex = function (keyName, key) {
//gets the actual items index for the row key
//see here http://stackoverflow.com/questions/21631127/find-the-array-index-of-an-object-with-a-specific-key-value-in-underscore
//for the 'getIndexBy' prototype extension
var ix = $scope.items.getIndexBy(keyName, key);
return ix;
}
$scope.checkAll = function () {
//only visible rows
var boxes = document.getElementsByClassName("rowSelector");
for (var bix in boxes) {
var ix = boxes[bix].value;
$scope.items[ix].isSelected = $scope.masterCheck;
}
}
There is probably a better way but while not highly efficient this one works well enough for common folk.
The $scope.sourceIndex() function stuffs the actual source row index into the row checkbox as its value= attribute value.
The checkAll() function then grabs all visible rows by the "rowSelector" class and then iterates through those grabbing the data index from the checkbox value and setting the appropriate item.isSelected.
The $scope.onPageChange is specified in the dir-Paginate controls directive and ensures that when the page changes, all row selections are cleared.
Happy happy.
Your variable masterCheck is being set to false in rowSelect() and subsequently in checkAll() you are setting checked to masterCheck which then is used to assign isSelected
This line is wrong:
var checked = $scope.masterCheck == true;
Because you want to flip masterCheck so it should be:
$scope.masterCheck = !$scope.masterCheck;
and then
.isSelected = $scope.masterCheck;
You weren't ever setting $scope.masterCheck to true so it was always false and since your isSelected values depended on it, they were always false. Also, this functions as a checkAll/unCheckAll to make it only check all change to the following:
$scope.masterCheck = !$scope.masterCheck;
var checked = $scope.masterCheck == true;

Count repeated items in an observableArray in knockout js

i have an observableArray with about 540 records
in the records it has an advert property and a date property i would like to do a function that finds all the adverts with on a particular date and counts the number of records eg
Date = 23/02/2015
Advert 1 (16)
Advert 2 (5)
Advert 3 (10)
Total (31)
This should help you get along, but basically you can have a function that iterates through the object and creates an array of counts:
self.counts = ko.observable();
function updateCounts() {
var leads = self.leads(),
counts = {};
for (var i = 0, j = leads.length; i < j; i++) {
var prop = leads[i].date_enquired();
if (counts[prop] == null) {
counts[prop] = {
name : prop,
count : 1
};
}
else {
counts[prop].count++;
}
}
// set it as an array instead of an object
self.counts(Object.keys(counts).map(function (key) { return counts[key]; }));
}
Run this function at the start and whenever the list updates:
updateCounts();
self.leads.subscribe(updateCounts);
Then display the object in the view in whatever way you'd like:
<div data-bind="foreach: counts">
<div data-bind="text: name + ': ' + count"></div>
</div>
JSFiddle
You could also create your own object that handles pushing to the array and updating the list of counts. That way additions are O(1) instead of O(n) each time an item is added or removed. Overall, it's not too big of a deal because iterating over 500 items is really fast.

Prepare array of objects before passing it to Angularjs ng-repeat to avoid '10 $digest() iterations reached' error

I have a following ng-repeat in my view:
<div ng-repeat="field in fields">
{{field.someValue}}
</div>
The content of fields needs to be preprocessed before it is given to the view. So in my controller I have a function that loops through the fields object and adds some keys and removes some keys. A simplified pseudocode would look like that
myApp.controller('FieldsController', function($scope) {
$scope.fields = loadFieldsFromResource();
var i=0;
for(i = 0; i < $scope.fields.length; i++) {
if ($scope.fields[i].someProperty > maxValue) {
// Remove an item from the array
$scope.fields.splice(i,1);
}
else if ($scope.fields[i].someProperty < minValue) {
// Add an item to the array
$scope.fields.splice(i,0,createNewField());
}
}
})
Now this produces correct output but gives me the 10 $digest() iterations reached. error.
How can I make it work? (I only need the preprocessing done on init).
I've tried to copy the fields with angular.copy() to a temporary variable. Do the preprocessing on it and then assigning it to the fields variable but still I get the same error.
Is there a way of doing this sort of preprocessing outside of the Angular watch before I give it to the view?
Could you use ng-init?
<div ng-init="processFields()" ng-repeat="field in fields">
{{field.someValue}}
</div>
And then in your controller you could do the following:
myApp.controller('FieldsController', function ($scope) {
$scope.fields = [];
$scope.processFields = function () {
var fields = loadFieldsFromResource();
var i = 0;
for (i = 0; i < fields.length; i++) {
if (fields[i].someProperty > maxValue) {
// Remove an item from the array
fields.splice(i, 1);
}
else if (fields[i].someProperty < minValue) {
// Add an item to the array
fields.splice(i, 0, createNewField());
}
}
$scope.fields = fields;
}
})
I don't know your entire situation but I have few places where I need things loaded and sorted before I display.

ng-repeat does not seem to rebind ng-click on a re-rendering

I have an ng-repeat assigned to a row in a table as shown below. When the user selects a down arrow in the row, the method moveDown gets executed, which reorders the list (see code).
When I look at the DOM, everything looks right - The rows are reordered, and the ng-click sees the newly assigned seqNbr.
Better explanation:
Initially first row shows data-ng-click='moveDown(0);' second data-ng-click='moveDown(1);'
After selecting the first one, the first and second row trade places. The seqNbr are swapped in the objects and list is reordered, then the ng-repeate is reexecuted.
Now the DOM shows that the NEW first row has: data-ng-click='moveDown(0);' and the old first row, now the second row, has data-ng-click='moveDown(1);'
However if I select the new first row, what gets executed is moveDown(1) (the old method associated with that row). Its as if the DOM is updated, but not the method binding.
HTML:
<tr class='evidencerow' data-ng-repeat="e in data.evidence">
<td><div class='assertion webdiv' style='height:4em;'
data-ng-dblclick='openReference(e);'>
<span data-ng-bind-html-unsafe='e.assertion'></span>
</div>
</td>
<td>
<img src='img/UpArrow16x16.png' data-ng-hide='$first'
data-ng-click='moveUp({{e.seqNbr}});' style='width:32px;'>
<img src='img/DownArrow16x16.png' data-ng-hide='$last'
data-ng-click='moveDown({{e.seqNbr}});' style='width:32px;'>
</td>
</tr>
controller code:
$scope.moveUp = function(seq) {
var recs = $scope.data.evidence.slice(0);
recs[seq].seqNbr = seq - 1;
if (_ev.notEmpty(recs[seq - 1])) {
var s2 = seq - 1;
recs[s2].seqNbr = seq;
}
recs.sort(_ev.compareSeqNbr);
$scope.data.evidence = recs;
};
$scope.moveDown = function(seq) {
var recs = $scope.data.evidence.slice(0);
recs[seq].seqNbr = seq + 1;
if (_ev.notEmpty(recs[seq + 1])) {
var s2 = seq +1;
recs[s2].seqNbr = seq;
}
recs.sort(_ev.compareSeqNbr);
$scope.data.evidence = recs;
};
This behavior doesn't seem right to me. The result is instead of the rows moving up and down, they toggle back and forth.
Try out this jsFiddle. I think it does what you're looking for.
I modified your moveUp() and moveDown() functions a bit and they now take the full "evidence" object rather than just a number.
$scope.moveUp = function(e) {
var idx = $scope.data.evidence.indexOf(e);
var removed = $scope.data.evidence.splice(idx, 1);
$scope.data.evidence.splice(idx - 1, 0, removed[0]);
};
$scope.moveDown = function(e) {
var idx = $scope.data.evidence.indexOf(e);
var removed = $scope.data.evidence.splice(idx, 1);
$scope.data.evidence.splice(idx + 1, 0, removed[0]);
};

Resources