View doesn't updated on element event - angularjs

I need show caret position of my editor in some place of website. I made directive and service to share variables between controller and directice. Inside directive I turned on events like "keyup", "mouseup" etc. This events fires, service is getting updated to correct values and angular controller see this value changed but view doesn't refreshed.
I quess that this event's doesn't inform angular to refresh view. How to do it properly? here is my code.
It can be important too that this directive called "EditorEvents" is placed in different place. Both has only common root (aren't nested)
class EditorEvents {
public link: (scope: IExpressionEditor, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => void;
private service: DebugValuesService;
constructor(text: string, service: DebugValuesService) {
this.service = service;
EditorEvents.prototype.link = (scope: IExpressionEditor, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
element.val(text);
element.on("input propertychange", (event: JQueryEventObject) => {
console.log('INFO: expression changed, new value: ', element.val());
this.setServiceValues(scope, element);
});
element.on("keyup", (event: JQueryEventObject) => {
if (event.which >= 37 && event.which <= 40) {
console.log('INFO: caret changed, new value: ', EditorEvents.GetCaretPosition(<HTMLInputElement>element[0]));
this.setServiceValues(scope, element);
}
});
element.on("mouseup", (evet: JQueryEventObject) => {
console.log('INFO: caret changed, new value: ', EditorEvents.GetCaretPosition(<HTMLInputElement>element[0]));
this.setServiceValues(scope, element);
});
//this.setServiceValues(scope, element);
};
}
private setServiceValues(scope: IExpressionEditor, element: ng.IAugmentedJQuery) {
var cursor = EditorEvents.GetCaretPosition(<HTMLInputElement>element[0]);
var text = element.val();
this.service.SetCursorPosition(cursor);
this.service.SetScriptLength(text.length);
this.service.Text = text;
}
private static GetCaretPosition(element: HTMLInputElement): number {
...
}
public static Factory(text: string) {
var directive = (service: DebugValuesService) => {
return new EditorEvents(text, service);
};
directive['$inject'] = [];
return directive;
}
}
and associated with this controller service
class DebugModeController extends BaseController<IDebugObject> {
constructor($scope: IDebugObject, service: DebugValuesService, $interval) {
super($scope, "DebugModeController");
$scope.IsDebugMode = () => Configuration.IsDebugMode;
$scope.Service = service;
}
}
This values should be visible here:
<div ng-controller="DebugModeController" class="debug-controller" ng-show="{{ IsDebugMode() }}">
<input disabled="disabled" type="hidden" ng-model="Service.ScriptLength" />
<input disabled="disabled" type="hidden" ng-model="Service.CursorPosition" />
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
</tr>
</thead>
<tbody>
<tr>
<td>Script length</td>
<td>{{ Service.ScriptLength }}</td>
</tr>
<tr>
<td>Cursor position</td>
<td>{{ Service.CursorPosition }}</td>
</tr>
</tbody>
</table>
</div>

I quess that this event's doesn't inform angular to refresh view. How to do it properly? here is my code.
You need to trigger angular 1.x dirty checking using a digest e.g. change:
element.on("mouseup", (evet: JQueryEventObject) => {
console.log('INFO: caret changed, new value: ', EditorEvents.GetCaretPosition(<HTMLInputElement>element[0]));
to:
element.on("mouseup", (evet: JQueryEventObject) => {
console.log('INFO: caret changed, new value: ', EditorEvents.GetCaretPosition(<HTMLInputElement>element[0]));
scope.$apply();

Related

AngularJS ng-click not responding when clicked

I know there is alot of questions about this topic out there, but could not find any solution to my problem. My code:
UPDATE
$scope.openOne = function (id) {
ImageService.getDetails(id).then(function (data) {
$scope.imageDetail = data;
}).catch(function (e) {
var message = [];
});
}
function getAllImages() {
ImageService.getImages().then(function (value) {
$scope.images = value;
var items = [];
$(value).each(function () {
var url = "https://someUrl/" + this.Image[0];
console.log(url);
items.push(`
<tr>
<td><button id="button" ng-click="openOne(${this._id})">${this.ImageName}</button></td>
<td>${this.ImageCategory}</td>
<td>
<img style="width:30%;" ng-src="${url}" alt="The Image is missing">
</td>
</tr>
`);
});
$("#body").append(items.join(''));
}).catch(function (e) {
var message = [];
}).finally(function (e) {
});
}
I am creating the button in in the controller and then appending it to the DOM.
Does anybody see the error? When I click the button nothing happens.
Approach is all wrong.
The fundamental principal in angular is let your data model drive the view and let angular compile that view from templates
A more typical set up would pass your images array to ng-repeat in the view:
Controller:
function getAllImages() {
ImageService.getImages().then(function (value) {
$scope.images = value;
});
}
View:
<tr ng-repeat="img in images track by $index">
<td><button id="button" ng-click="openOne(img.id)">{{img.ImageName}}</button></td>
<td>{{img.ImageCategory}}</td>
<td>
<img style="width:30%;" ng-src="{{img.url}}" alt="The Image is missing">
</td>
</tr>
You need to add $compile service here, that will bind the angular directives like ng-click to your controller scope. For example
app.controller('yourController', function($scope,$compile) {
var button = '<button id="button" ng-click="openOne(${this._id})">${this.ImageName}</button>';
items.push(button);
$("#body").append(items.join(''));
var temp = $compile(button)($scope);
$scope.openOne = function(){
alert('Yes Click working at dynamically added element');
}
});

Click event of Typescript with angular not getting object value

Getting issue with binding value while passing Event parameter from Template to Typescript Button click event.
See below object, its binding but it not reflecting same to the Typescript controller..
See here that value..
What it should be reason?
Code:
Get Order template
<table class="table table-bordered">
<tr>
<td>ID </td>
<td>Name</td>
<td>Price</td>
<td>Action</td>
</tr>
<tr ng-repeat="order in vm.addNewOrderData track by $index">
<td><label>{{order.orderID}}</label> </td>
<td><label>{{order.orderName}}</label> </td>
<td><label>{{order.orderPrice}}</label> </td>
<td><label> Edit</label> </td>
</tr>
</table>
below is GetOrderController.ts
class GetOrderController {
public vm: any;
private scope: any;
public addNewOrderData: any;
public http: any;
public location: any;
constructor($http: any, $scope: any, $location:any) {
this.scope = $scope;
this.scope.vm = this;
this.http = $http;
this.location = $location;
$http.get("/api/SampleApi/GetAllOrder").success(
(data, status) => {
if (data != "null") {
this.addNewOrderData = data;
}
}).error((data, status) => {
this.addNewOrderData = data;
});
}
edit = (id: any): void => {
debugger;
var order = { OrderID: id };
this.http.get("/api/SampleApi/GetOrderById",id
).success(
(data, status) => {
if (data != "null") {
this.location.path('/saveOrder?id=' + order);
}
else {
alert("No order id found!");
}
}).error((data, status) => {
});
}
}
ng-click shouldn't have {{}}(interpolation), do pass direct expression while will get evaluate.
Change
ng-click="vm.edit('{{order.orderID}}');"
to
ng-click="vm.edit(order.orderID);"

Angularjs Service does not work

I define a Service to share a variable between two controllers, but when i set the variable in a controller and then get this from another controller it does not get the correct value , this is the service:
App.service("ProductService", function () {
var productTotalCount = {};
return {
getproductTotalCount: function () {
return productTotalCount;
},
setproductTotalCount: function (value) {
productTotalCount = value;
}
}
});
and this is the controller which i set productTotalCount:
App.controller("ProductController", function ($scope, $http, $rootScope, ProductService) {
$scope.GetAllProducts = $http.get("GetAllProductsInformation").success(function (data) {
$rootScope.Products = data.Data;
ProductService.setproductTotalCount(data.TotalCount); // i set productTotalCount here and it's value became 19
});
$scope.editProduct = function (data) {
$scope.model = data;
$rootScope.$broadcast('modalFire', data)
}
});
and when i get the productTotalCount in this controller it return object instead of 19 :
App.controller('Pagination', function ($scope, ProductService) {
debugger;
$scope.totalItems = ProductService.getproductTotalCount(); // it should return 19 but return object!!
$scope.currentPage = 1;
$scope.itemPerPage = 8;
});
what is the problem?
EDIT: this is the html, it may help :
<div ng-controller="ProductController" ng-init="GetAllProducts()">
<div class="row" style="margin-top:90px" ng-show="!ShowGrid">
<article class="widget">
<header class="widget__header">
<div class="widget__title">
<i class="pe-7s-menu"></i><h3>ProductList</h3>
</div>
<div class="widget__config">
<i class="pe-7f-refresh"></i>
<i class="pe-7s-close"></i>
</div>
</header>
<div class="widget__content table-responsive">
<table class="table table-striped media-table">
<thead style="background-color:rgba(33, 25, 36,0.1)">
<tr>
<th style="width:40%">edit</th>
<th style="width:30%">Price</th>
<th style="width:30%">ProductName</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="product in Products">
#*<td>{{product.ProductDescription}}</td>*#
<td>
<input class="btn btn-default" style="padding: 14px;background: rgba(0, 0, 0, 0.2)" type="submit" value="Edit" ng-click="editProduct(product)" />
</td>
<td>{{product.Price}}</td>
<td>{{product.ProductName}}</td>
</tr>
</tbody>
</table>
</div>
</article>
</div>
</div>
<div ng-controller="Pagination">
<pagination total-items="totalItems" ng-change="pageChanged()" previous-text="Before" next-text="Next" first-text="First"
last-text="Last" ng-model="currentPage" items-per-page="itemPerPage" max-size="maxSize" class="pagination-sm" boundary-links="true"></pagination>
</div>
From the controller names, I bet the Pagination and ProductController controllers are both instantiated more or less at the same time, BEFORE invoking the .setproductTotalCount() method. If that is the case, then because you are treating the productTotalCount variable as a primitive type (instead of an object) after setting it, the changes do not get reflected between the controllers.
Try the following:
// Change the service to:
App.service("ProductService", function () {
var productTotalCount = {};
return {
getproductTotalCount: function () {
return productTotalCount;
},
setproductTotalCount: function (value) {
productTotalCount.value = value;
}
}
});
// In Pagination controller:
App.controller('Pagination', function ($scope, ProductService) {
debugger;
$scope.totalItems = ProductService.getproductTotalCount(); // this will still be an empty object initially, but when the value is updated in the service, the $scope.totalItems will also be updated
$scope.currentPage = 1;
$scope.itemPerPage = 8;
// this should confirm that changes are being propagated.
$scope.$watch('totalItems', function(newVal) {
console.log('totalItems updated. New Value:', newVal);
});
// NOTE: Keep in mind that the real productTotalCount will be stored as $scope.totalItems.value;
});
---- EDIT ----
Per your comment below, it proves that the solution above DOES work. To prove it, change:
$scope.$watch('totalItems', function(newVal) {
console.log('totalItems updated. New Value:', newVal);
});
to
$scope.$watch('totalItems', function(newVal) {
console.log('totalItems updated. New Value:', newVal);
console.log($scope.totalItems);
});
At that point, you should see that $scope.totalItems has been updated to:
{ value: 19 };
The issue may be how you're declaring your variable in your service. Because it's a local variable in the function rather than returned object, I believe it will be creating a new variable for each time you inject the service as a dependency. Try making the variable a member of the returned object. E.g.
App.service("ProductService", function () {
return {
productTotalCount: 0,
getproductTotalCount: function () {
return this.productTotalCount;
},
setproductTotalCount: function (value) {
this.productTotalCount = value;
}
}
});

ui-grid Edit Feature not working properly

I am using ui-grid. I enabled edit feature using ui-grid-edit. The problem is datepicker.
I want the datepicker to work in all browsers but it is not possible by enabling the type : "date" because it will give the same datepicker supported by HTML5.
So i thought of enabling bootstrap datepicker for angular using custom template
by adding editableCellTemplate
columnDefs : {
field:'accbirthdate',displayName :"Birth Date",enableCellEditOnFocus:true,editableCellTemplate: '<input type="text" ng-input="COL_FIELD" ng-model="COL_FIELD" datepicker-popup="dd-MM-yyyy" datepicker-append-to-body="true"/>'}
}
but it is not working at all. I found that even ng-click in the input text also is not working.So anyone please help on how to enable bootstarp date-picker in angular ui-grid
Yes as i think it is because the HTML code is not compiled when we are using it directly on editableCellTemplate.
For more info on how Angular Template Compiling Works refer here
Angular Template Compiling
Here's what i do to resolve this issue. I replaced my column defs editableCellTemplate to the following
columnDefs : {
field:'accbirthdate',displayName :"Birth Date",enableCellEditOnFocus: true,editableCellTemplate: '<input type="text" class="form-control" datepicker-popup="dd/MM/yyyy" ng-class="\'colt\' + col.index" ng-model="row.entity[col.field]" datepicker-append-to-body="true" is-open="istableDate" close-text="Close" table-date/>'}
}
as like gridEditor i created my own directive in my directive.js i have created the directive
app.directive('tableDate',function($filter){
function parseDateString(dateString) {
if (typeof(dateString) === 'undefined' || dateString === '') {
return null;
}
var parts = dateString.split('/');
if (parts.length !== 3) {
return null;
}
var year = parseInt(parts[2], 10);
var month = parseInt(parts[1], 10);
var day = parseInt(parts[0], 10);
if (month < 1 || year < 1 || day < 1) {
return null;
}
return new Date(year, (month - 1), day);
}
function pad(s) { return (s < 10) ? '0' + s : s; }
return {
priority: -100, // run after default uiGridEditor directive
require: '?ngModel',
link: function (scope, element, attrs, ngModel) {
scope.istableDate = false;
scope.$on( 'uiGridEventBeginCellEdit', function () {
scope.istableDate = true;
});
element.on("click",function(){
scope.istableDate = true;
});
element.bind( 'blur', function () {
if(!scope.istableDate){
scope.$emit( 'uiGridEventEndCellEdit' );
}else{
scope.istableDate = false;
}
}); //when leaving the input element, emit the 'end cell edit' event
if (ngModel) {
ngModel.$formatters.push(function (modelValue) {
var modelValue= new Date(modelValue);
ngModel.$setValidity(null,(!modelValue || !isNaN(modelValue.getTime())));
return $filter('date')(modelValue, 'dd/MM/yyyy');
});
ngModel.$parsers.push(function (viewValue) {
if (viewValue ) {
var dateString = [pad(viewValue.getDate()), pad(viewValue.getMonth()+1), viewValue.getFullYear()].join('/')
var dateValue = parseDateString(dateString);
ngModel.$setValidity(null, (dateValue && !isNaN(dateValue.getTime())));
return dateValue;
}
else {
ngModel.$setValidity(null, true);
return null;
}
});
}
}
};
});
My Date format is dd/MM/yyyy, choose yours once we emit the event it will mentioned as changed.
Now i have my bootstrap date-picker working for my ui-grid on all browsers.
If you have any doubt on this feel free to ask me. i Spend One day on this and I would like to share my experience to others.
Oops not in IE6 :)
I got this same issue before one moth ago. but i can't find out the solution. So i change my mind to custom grid.
I have using table, tr,td tags
see this is my html code
<table ng-show="TimeSheetList.length!==0" class="table">
<tr>
<th style="background-color: #BBE1EF">Start Date</th>
<th style="background-color: #BBE1EF">Start Time</th>
<th style="background-color: #BBE1EF">End Time</th>
<th style="background-color: #BBE1EF">Total Hours</th>
<th style="background-color: #BBE1EF">Description</th>
<th style="background-color: #BBE1EF">Remarks</th>
</tr>
<tr ng-repeat="timesheetlist in TimeSheetList">
<td ng-dblclick="ChangeVal(this,'StartDate')"><span ng-hide="timesheetlist.IsEditStartDate">{{timesheetlist.StartDate}}</span>
<input style="width: 150px" type="text" class="form-control" date-time required="true" view="hours" partial="true" ng-blur="UpdateTimeSheet(this.timesheetlist)" id="StartDate{{$index}}" ng-show="timesheetlist.IsEditStartDate" ng-model="timesheetlist.StartDate" />
</td>
<td><span ng-dblclick="ChangeVal(this,'StartTime')" ng-hide="timesheetlist.IsEditStartTime">{{timesheetlist.StartTime}}</span>
<timepicker hour-step="1" minute-step="1" show-meridian="true" ng-change="UpdateTimeSheet(this.timesheetlist)" ng-blur="UpdateTimeSheet(this.timesheetlist)" id="StartTime{{$index}}" ng-show="timesheetlist.IsEditStartTime" ng-model="timesheetlist.StartTime"></timepicker>
</td>
<td><span ng-dblclick="ChangeVal(this,'EndTime')" ng-hide="timesheetlist.IsEditEndTime">{{timesheetlist.EndTime}}</span>
<timepicker hour-step="1" minute-step="1" show-meridian="true" ng-change="UpdateTimeSheet(this.timesheetlist)" ng-blur="UpdateTimeSheet(this.timesheetlist)" id="EndTime{{$index}}" ng-show="timesheetlist.IsEditEndTime" ng-model="timesheetlist.EndTime"></timepicker>
</td>
<td>
<input type="text" readonly="true" class="form-control" ng-model="timesheetlist.TotalHours" style="width: 200px" autofocus="">
</td>
<td><span ng-dblclick="ChangeVal(this,'Description')" ng-hide="timesheetlist.IsEditDescription">{{timesheetlist.Description}}</span>
<input style="width: 200px" type="text" class="form-control" ng-blur="UpdateTimeSheet(this.timesheetlist)" ng-show="timesheetlist.IsEditDescription" ng-model="timesheetlist.Description" /></td>
<td><span ng-dblclick="ChangeVal(this,'Remarks')" ng-hide="timesheetlist.IsEditRemarks">{{timesheetlist.Remarks}}</span>
<input style="width: 200px" type="text" class="form-control" ng-blur="UpdateTimeSheet(this.timesheetlist)" ng-show="timesheetlist.IsEditRemarks" ng-model="timesheetlist.Remarks" /></td>
</tr>
</table>
this is my controller code
function loadTimeSheets(date) {
$http({
method: 'GET',
url: rootUrl + '/api/TimeSheet/LoadTimeSheet?date=' + date,
headers: {
'Content-Type': "application/json; charset=utf-8"
}
}).success(function (response) {
$scope.TimeSheetList = response;
if ($scope.TimeSheetList.length == 0) {
alert('You did not add your works in this date');
}
}).error(function (response, errorCode) {
if (errorCode == 444) {
//toastr.error('Your email address is does not verrified ', 'Error');
}
})
}
I hope you can customize my grid with what you want.
If you don't like that, then you can go to with ng-Grid
I also got this problem with datepicker and decidet to add a default jquery-ui one. Actually, you can add anyone.
The code below requires some explanation.
First, you template should looks like that:
editableCellTemplate: '<input datepicker ng-model="MODEL_COL_FIELD" readonly>'
It must contain a ng-model attribute with a value MODEL_COL_FIELD. This value means that your input will be bind with a cell's model. I.e. the cell after stopping an edit mode the cell (it's usually a <div> element) will get the value from our input. The MODEL_COL_FIELD value is replaced by ui-grid engine with real name of model. In our case this is row.entity['startDate']. You can see this when you read a property $attrs.ngModel.
The
eval('$scope.' + $attrs.ngModel + ' = "' + value + '";');
line appears like that because we can't know a name of model. This name is designated with ui-grid engine automatically. Instead of eval you can create a function allowing to access a value of $scope parsing $attrs.ngModel value.
Second, when you create some directive, all DOM modifications should be replaced in the compile section.
Third, if you create your own directive for edit-mode in ui-grid, you must fire BEGIN_CELL_EDIT, CANCEL_CELL_EDIT and END_CELL_EDIT events manually (see http://ui-grid.info/docs/#/tutorial/201_editable). In our code we do this in onClose property of datepicker. We don't use onSelect because we can loose a focus on our input and the datepicker will be closed, but the input will be still opened. To stop an edit mode we must fire the END_CELL_EDIT event:
$scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
The JS code:
var app = angular.module('app', ['ui.grid', 'ui.grid.edit']);
app.controller('MainController', function($scope) {
$scope.gridOptions = {
columnDefs: [{
name: 'startDate',
editableCellTemplate: '<input datepicker ng-model="MODEL_COL_FIELD" readonly>',
enableCellEdit: true,
width: 150
}],
data: [{
startDate: ''
}]
};
});
app.directive('datepicker', ['uiGridEditConstants', function(uiGridEditConstants) {
return {
restrict: 'A',
require: 'ngModel',
compile: function() {
return {
pre: function($scope, $elm, $attrs) {},
post: function($scope, $elm, $attrs) {
function setValueToScope(value) {
eval('$scope.' + $attrs.ngModel + ' = "' + value + '";');
$scope.$apply();
}
$elm = $($elm);
$elm.datepicker({
dateFormat: 'mm/dd/yy',
onSelect: function(date) {
setValueToScope(date);
},
onClose: function() {
$scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
}
});
$elm[0].focus();
}
};
}
};
}]);
The full code and how it is work you can see here: http://plnkr.co/edit/NfMuGpNDqIjvoAGJ2R1B

Navigate the UI using only keyboard

I'm trying to navigate thru a list of records using only keyboard. When the page loads, the default "focus" should be on the first record, when the user clicks the down arrow on the keyboard, the next record needs to be focused. When the user clicks the up arrow, the previous record should be focused. When the user clicks the Enter button, it should take them to the details page of that record.
Here's what I have so far on Plunkr.
It appears that this is supported in AngularJS in 1.1.5 (unstable), which we can't use in production. I'm currently using 1.0.7. I'm hoping to do something like this - the key should be handled at the document level. When the user presses a certain key, the code should look up in an array of allowed keys. If a match is found (e.g. down key code), it should move the focus (apply the .highlight css) to the next element. When enter is pressed, it should grab the record which .highlight css and get the record id for further processing.
Thank you!
Here is the example what you could choose to do:
http://plnkr.co/edit/XRGPYCk6auOxmylMe0Uu?p=preview
<body key-trap>
<div ng-controller="testCtrl">
<li ng-repeat="record in records">
<div class="record"
ng-class="{'record-highlight': record.navIndex == focu sIndex}">
{{ record.name }}
</div>
</li>
</div>
</body>
This is the simplest approach I could think of.
It binds a directive keyTrap to the body which catches the keydown event and
$broadcast message to child scopes.
The element holder scope will catch the message and simply increment or decrement the
focusIndex or fire an open function if hitting enter.
EDIT
http://plnkr.co/edit/rwUDTtkQkaQ0dkIFflcy?p=preview
now supports, ordered / filtered list.
Event handling part has not changed, but now uses $index and also filtered list caching
technique combined to track which item is getting focused.
All of the solutions offered up so far have a single common problem. The directives are not reusable, they require knowledge of variables created in the parent $scope provided by the controller. That means if you wanted to use the same directive in a different view you would need to re-implement everything you did the previous controller and ensure you are using the same variable names for things, since the directives basically have hard coded $scope variable names in them. You definitely wouldn’t be able to use the same directive twice within the same parent scope.
The way around this is to use isolated scope in the directive. By doing this you can make the directive reusable regardless of the parent $scope by generically parameterizing items required from the parent scope.
In my solution the only thing that the controller needs to do is provide a selectedIndex variable that the directive uses to track which row in the table is currently selected. I could have isolated the responsibility of this variable to the directive but by making the controller provide the variable it allows you to manipulate the currently selected row in the table outside of the directive. For example you could implement “on click select row” in your controller while still using the arrow keys for navigation in the directive.
The Directive:
angular
.module('myApp')
.directive('cdArrowTable', cdArrowTable);
.directive('cdArrowRow', cdArrowRow);
function cdArrowTable() {
return {
restrict:'A',
scope: {
collection: '=cdArrowTable',
selectedIndex: '=selectedIndex',
onEnter: '&onEnter'
},
link: function(scope, element, attrs, ctrl) {
// Ensure the selectedIndex doesn't fall outside the collection
scope.$watch('collection.length', function(newValue, oldValue) {
if (scope.selectedIndex > newValue - 1) {
scope.selectedIndex = newValue - 1;
} else if (oldValue <= 0) {
scope.selectedIndex = 0;
}
});
element.bind('keydown', function(e) {
if (e.keyCode == 38) { // Up Arrow
if (scope.selectedIndex == 0) {
return;
}
scope.selectedIndex--;
e.preventDefault();
} else if (e.keyCode == 40) { // Down Arrow
if (scope.selectedIndex == scope.collection.length - 1) {
return;
}
scope.selectedIndex++;
e.preventDefault();
} else if (e.keyCode == 13) { // Enter
if (scope.selectedIndex >= 0) {
scope.collection[scope.selectedIndex].wasHit = true;
scope.onEnter({row: scope.collection[scope.selectedIndex]});
}
e.preventDefault();
}
scope.$apply();
});
}
};
}
function cdArrowRow($timeout) {
return {
restrict: 'A',
scope: {
row: '=cdArrowRow',
selectedIndex: '=selectedIndex',
rowIndex: '=rowIndex',
selectedClass: '=selectedClass',
enterClass: '=enterClass',
enterDuration: '=enterDuration' // milliseconds
},
link: function(scope, element, attrs, ctr) {
// Apply provided CSS class to row for provided duration
scope.$watch('row.wasHit', function(newValue) {
if (newValue === true) {
element.addClass(scope.enterClass);
$timeout(function() { scope.row.wasHit = false;}, scope.enterDuration);
} else {
element.removeClass(scope.enterClass);
}
});
// Apply/remove provided CSS class to the row if it is the selected row.
scope.$watch('selectedIndex', function(newValue, oldValue) {
if (newValue === scope.rowIndex) {
element.addClass(scope.selectedClass);
} else if (oldValue === scope.rowIndex) {
element.removeClass(scope.selectedClass);
}
});
// Handles applying/removing selected CSS class when the collection data is filtered.
scope.$watch('rowIndex', function(newValue, oldValue) {
if (newValue === scope.selectedIndex) {
element.addClass(scope.selectedClass);
} else if (oldValue === scope.selectedIndex) {
element.removeClass(scope.selectedClass);
}
});
}
}
}
This directive not only allows you to navigate a table using the arrow keys but it allows you to bind a callback method to the Enter key. So that when the enter key is pressed the row that is currently selected will be included as an argument to the callback method registered with the directive (onEnter).
As a little bit of an added bonus you can also pass a CSS class and duration to the cdArrowRow directive so that when the enter key is hit on a selected row the CSS class passed in will be applied to the row element then removed after the passed in duration (in milliseconds). This basically allows you to do something like making the row flash a different color when the enter key is hit.
View Usage:
<table cd-arrow-table="displayedCollection"
selected-index="selectedIndex"
on-enter="addToDB(row)">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in displayedCollection"
cd-arrow-row="row"
selected-index="selectedIndex"
row-index="$index"
selected-class="'mySelcetedClass'"
enter-class="'myEnterClass'"
enter-duration="150"
>
<td>{{row.firstName}}</td>
<td>{{row.lastName}}</td>
</tr>
</tbody>
</table>
Controller:
angular
.module('myApp')
.controller('MyController', myController);
function myController($scope) {
$scope.selectedIndex = 0;
$scope.displayedCollection = [
{firstName:"John", lastName: "Smith"},
{firstName:"Jane", lastName: "Doe"}
];
$scope.addToDB;
function addToDB(item) {
// Do stuff with the row data
}
}
This is the directive below that I had once build for a similar problem.
This directive listens to the keyboard events and changes the row selection.
This link has a complete explanation on how to build it. Change row selection using arrows.
Here is the directive
foodApp.directive('arrowSelector',['$document',function($document){
return{
restrict:'A',
link:function(scope,elem,attrs,ctrl){
var elemFocus = false;
elem.on('mouseenter',function(){
elemFocus = true;
});
elem.on('mouseleave',function(){
elemFocus = false;
});
$document.bind('keydown',function(e){
if(elemFocus){
if(e.keyCode == 38){
console.log(scope.selectedRow);
if(scope.selectedRow == 0){
return;
}
scope.selectedRow--;
scope.$apply();
e.preventDefault();
}
if(e.keyCode == 40){
if(scope.selectedRow == scope.foodItems.length - 1){
return;
}
scope.selectedRow++;
scope.$apply();
e.preventDefault();
}
}
});
}
};
}]);
<table class="table table-bordered" arrow-selector>....</table>
And your repeater
<tr ng-repeat="item in foodItems" ng-class="{'selected':$index == selectedRow}">
I had a similar requirement to support UI navigation using arrow keys. What I finally came up with is DOM's keydown event handler encapsulated within an AngularJS directive:
HTML:
<ul ng-controller="MainCtrl">
<li ng-repeat="record in records">
<div focusable tag="record" on-key="onKeyPressed" class="record">
{{ record.name }}
</div>
</li>
</ul>
CSS:
.record {
color: #000;
background-color: #fff;
}
.record:focus {
color: #fff;
background-color: #000;
outline: none;
}
JS:
module.directive('focusable', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.attr('tabindex', '-1'); // make it focusable
var tag = attrs.tag ? scope.$eval(attrs.tag) : undefined; // get payload if defined
var onKeyHandler = attrs.onKey ? scope.$eval(attrs.onKey) : undefined;
element.bind('keydown', function (event) {
var target = event.target;
var key = event.which;
if (isArrowKey(key)) {
var nextFocused = getNextElement(key); // determine next element that should get focused
if (nextFocused) {
nextFocused.focus();
event.preventDefault();
event.stopPropagation();
}
}
else if (onKeyHandler) {
var keyHandled = scope.$apply(function () {
return onKeyHandler.call(target, key, tag);
});
if (keyHandled) {
event.preventDefault();
event.stopPropagation();
}
}
});
}
};
});
function MainCtrl ($scope, $element) {
$scope.onKeyPressed = function (key, record) {
if (isSelectionKey(key)) {
process(record);
return true;
}
return false;
};
$element.children[0].focus(); // focus first record
}
You could create a table navigation service which tracks the current row and exposes navigation methods to modify the current row's value and sets focus to the row.
Then all you would need to do is create a key binding directive where you could track key down events and fire the exposed methods from the table navigation service, on key up or key down.
I have used a controller to link the service methods to the key binding directive via a configuration object called 'keyDefinitions'.
You can extend the keyDefinitions to include the Enter key (Code: 13) and hook on to the selected $index value via the service property 'tableNavigationService.currentRow' or '$scope.data', then pass it as a parameter to your own custom submit() function.
I hope that this is helpful to somebody.
I have posted my solution to this issue at the following plunker location:
Keyboard Navigation Service Demo
HTML:
<div key-watch>
<table st-table="rowCollection" id="tableId" class="table table-striped">
<thead>
<tr>
<th st-sort="firstName">first name</th>
<th st-sort="lastName">last name</th>
<th st-sort="birthDate">birth date</th>
<th st-sort="balance" st-skip-natural="true">balance</th>
<th>email</th>
</tr>
</thead>
<tbody>
<!-- ADD CONDITIONAL STYLING WITH ng-class TO ASSIGN THE selected CLASS TO THE ACTIVE ROW -->
<tr ng-repeat="row in rowCollection track by $index" tabindex="{{$index + 1}}" ng-class="{'selected': activeRowIn($index)}">
<td>{{row.firstName | uppercase}}</td>
<td>{{row.lastName}}</td>
<td>{{row.birthDate | date}}</td>
<td>{{row.balance | currency}}</td>
<td>
<a ng-href="mailto:{{row.email}}">email</a>
</td>
</tr>
</tbody>
</table>
</div>
CONTROLLER:
app.controller('navigationDemoController', [
'$scope',
'tableNavigationService',
navigationDemoController
]);
function navigationDemoController($scope, tableNavigationService) {
$scope.data = tableNavigationService.currentRow;
$scope.keyDefinitions = {
'UP': navigateUp,
'DOWN': navigateDown
}
$scope.rowCollection = [
{
firstName: 'Chris',
lastName: 'Oliver',
birthDate: '1980-01-01',
balance: 100,
email: 'chris#email.com'
},
{
firstName: 'John',
lastName: 'Smith',
birthDate: '1976-05-25',
balance: 100,
email: 'chris#email.com'
},
{
firstName: 'Eric',
lastName: 'Beatson',
birthDate: '1990-06-11',
balance: 100,
email: 'chris#email.com'
},
{
firstName: 'Mike',
lastName: 'Davids',
birthDate: '1968-12-14',
balance: 100,
email: 'chris#email.com'
}
];
$scope.activeRowIn = function(index) {
return index === tableNavigationService.currentRow;
};
function navigateUp() {
tableNavigationService.navigateUp();
};
function navigateDown() {
tableNavigationService.navigateDown();
};
function init() {
tableNavigationService.setRow(0);
};
init();
};
})();
SERVICE AND DIRECTIVE:
(function () {
'use strict';
var app = angular.module('tableNavigation', []);
app.service('tableNavigationService', [
'$document',
tableNavigationService
]);
app.directive('keyWatch', [
'$document',
keyWatch
]);
// TABLE NAVIGATION SERVICE FOR NAVIGATING UP AND DOWN THE TABLE
function tableNavigationService($document) {
var service = {};
// Your current selected row
service.currentRow = 0;
service.table = 'tableId';
service.tableRows = $document[0].getElementById(service.table).getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// Exposed method for navigating up
service.navigateUp = function () {
if (service.currentRow) {
var index = service.currentRow - 1;
service.setRow(index);
}
};
// Exposed method for navigating down
service.navigateDown = function () {
var index = service.currentRow + 1;
if (index === service.tableRows.length) return;
service.setRow(index);
};
// Expose a method for altering the current row and focus on demand
service.setRow = function (i) {
service.currentRow = i;
scrollRow(i);
}
// Set focus to the active table row if it exists
function scrollRow(index) {
if (service.tableRows[index]) {
service.tableRows[index].focus();
}
};
return service;
};
// KEY WATCH DIRECTIVE TO MONITOR KEY DOWN EVENTS
function keyWatch($document) {
return {
restrict: 'A',
link: function(scope) {
$document.unbind('keydown').bind('keydown', function(event) {
var keyDefinitions = scope.keyDefinitions;
var key = '';
var keys = {
UP: 38,
DOWN: 40,
};
if (event && keyDefinitions) {
for (var k in keys) {
if (keys.hasOwnProperty(k) && keys[k] === event.keyCode) {
key = k;
}
}
if (!key) return;
var navigationFunction = keyDefinitions[key];
if (!navigationFunction) {
console.log('Undefined key: ' + key);
return;
}
event.preventDefault();
scope.$apply(navigationFunction());
return;
}
return;
});
}
}
}
})();

Resources