Navigate the UI using only keyboard - angularjs

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;
});
}
}
}
})();

Related

AngularJS: Cloning element containing ng-repeat

I am creating a directive with Angular 1.6 which creates a fixed header for a table. I am trying to achieve this by clone the table header and fixing this. This all works fine in most of my tables. I am using scope: false to keep parent scope, as some header elements references e.g. sorting functions. This also works. My problem is with one table I have which creates columns based on an array, because I want to be able to change columns. The columns are added with ng-repeat. When I clone this header, the ng-repeat is not cloned.
What can I do to clone an element containing an ng-repeat?
HTML of table:
<table class="proloen-table no-last-border" table-fix-header>
<thead class="light-blue-background">
<tr>
<th>{{vm.testString}}</th>
<th ng-repeat="head in vm.tableHeaders">
<span>{{ head.label | translate }}</span>
<sorting sortkey="head.sort" color="'white'" filter="vm.state.sorting"></sorting>
</th>
</tr>
</thead>
...
</table>
Controller (with controllerAs: 'vm') has (among other things):
vm.testString = 'Test';
vm.tableHeaders = [{label: 'Column1', sort: 'prop1'}, {label: 'Column2', sort: 'prop2'}];
The directive is as follows:
.directive('tableFixHeader', function ($window, $compile) {
return {
restrict: 'A',
scope: false,
link: function (scope, element) {
var clone;
function init() {
element.wrap('<div class="fix-table-container"></div>');
clone = element.clone(true);
clone.find('tbody').remove().end().addClass('table-header-fixed');
clone.removeAttr('table-fix-header');
$compile(clone)(scope);
element.before(clone);
resizeFixed();
}
function resizeFixed() {
clone.find('th').each(function (index) {
$(this).css('width', element.find('th').eq(index).outerWidth() + 'px');
});
}
function scrollFixed() {
var offset = $($window).scrollTop(),
tableOffsetTop = element.offset().top,
tableOffsetBottom = tableOffsetTop + element.height() - element.find('thead').height();
if (offset < tableOffsetTop || offset > tableOffsetBottom){
clone.hide();
}
else if (offset >= tableOffsetTop && offset <= tableOffsetBottom && clone.is(':hidden')) {
clone.show();
}
}
$window.addEventListener('scroll', scrollFixed);
$window.addEventListener('resize', resizeFixed);
scope.$on('$destroy', function() {
$window.removeEventListener('scroll', scrollFixed);
$window.removeEventListener('resize', resizeFixed);
});
init();
}
};
});
The directive works fine for tables columns are fixed and the above example clones the first "hardcoded" column just fine, along the variable from controller. The problem arises when cloning the ng-repeat. I just can't seem to figure out how to clone the ng-repeat, so that it to will work and update when I update the list of columns.
You could try to send event with $scope.$emit when ng-repeat finished rendering. Or create own event emitter and connect your directives;
app.directive('onFinishRepeat', function(){
return {
restrict: 'A',
link: function($scope) {
if($scope.$last == true) {
$scope.$emit('ng-repeat', 'finish');
}
}
}
})
app.directive('tableFixHeader', function ($window, $compile) {
return {
restrict: 'A',
scope: false,
link: function (scope, element) {
var clone;
function init() {
element.wrap('<div class="fix-table-container"></div>');
clone = element.clone(true);
clone.find('tbody').remove().end().addClass('table-header-fixed');
clone.removeAttr('table-fix-header');
$compile(clone)(scope);
element.before(clone);
resizeFixed();
}
function resizeFixed() {
clone.find('th').each(function (index) {
$(this).css('width', element.find('th').eq(index).outerWidth() + 'px');
});
}
function scrollFixed() {
var offset = $($window).scrollTop(),
tableOffsetTop = element.offset().top,
tableOffsetBottom = tableOffsetTop + element.height() - element.find('thead').height();
if (offset < tableOffsetTop || offset > tableOffsetBottom){
clone.hide();
}
else if (offset >= tableOffsetTop && offset <= tableOffsetBottom && clone.is(':hidden')) {
clone.show();
}
}
$window.addEventListener('scroll', scrollFixed);
$window.addEventListener('resize', resizeFixed);
scope.$on('$destroy', function() {
$window.removeEventListener('scroll', scrollFixed);
$window.removeEventListener('resize', resizeFixed);
});
$scope.on('ng-repeat', function(event, data){
if(data == 'finish') {
init();
}
})
}
};
});
HTML
<table class="proloen-table no-last-border" table-fix-header>
<thead class="light-blue-background">
<tr>
<th>{{vm.testString}}</th>
<th ng-repeat="head in vm.tableHeaders" on-finish-repeat>
<span>{{ head.label | translate }}</span>
<sorting sortkey="head.sort" color="'white'" filter="vm.state.sorting"></sorting>
</th>
</tr>
</thead>
...
</table>

View doesn't updated on element event

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();

How to pass dynamic object in angularjs directive

I am trying to generate multiple chart on single page with ng-repeat. The directive to achieve this accept chart data object as below.
module.directive('miChart', function () {
return {
restrict: 'E',
template: '<div></div>',
scope: {
chartData: "=value",
chartObj: "=?"
},
replace: true,
link: function ($scope, $element, $attrs) {
$scope.$watch('chartData', function (value) {
if (!value) {
return;
}
$scope.chartData.chart.renderTo = $scope.chartData.chart.renderTo || $element[0];
$scope.chartObj = new Highcharts.Chart($scope.chartData);
});
}
}
});
The basic html is as below.
<table>
<tr>
<td>
<mi-chart value="pieChartData" chart-obj="pieChartObj"></mi-chart>
</td>
<td>
<mi-chart ng-repeat="property in Property.Competitors" value="property.PropertyId" chart-obj="property.PropertyId"></mi-chart>
</td>
</tr>
</table>
Javascript code to assign values to scope.
dataPromise.then(function (serverResponse) {
var pieChart = serverResponse.data;
var pieChartData = getBaseChartData(pieChart.Primary);
var chartData = jQuery.extend(true, {}, pieChartData);
chartData = jQuery.extend(true, chartData, chartDiff);
$scope.pieChartData = chartData;
if (pieChart.Competitors.length > 0)
{
jQuery.each(pieChart.Competitors, function (index, CompData) {
var pieChartDataComp = getBaseChartData(CompData);
var chartDataComp = jQuery.extend(true, {}, pieChartDataComp);
chartDataComp = jQuery.extend(true, chartDataComp, chartDiff);
$scope[CompData.PropertyId] = chartDataComp;
}
});
}
});
In above code i am able to render my pieChartData with my directive.
But the code doesnt work in case of ng-repeat where i have to create dynamic object based on values in "Property.Competitors".
I can not use string(#) or one way binding(&) in my case as i have to deal with chart(highchart) object and watch functionality.
I am assigning dynamic scope values in my service with server side calls.
Can anybody help me out to generate multiple chart with ng-repeat and directive.
How can i create dynamic object and keep watch on that?

Issue with Popover AngularJS

I have a bunch of table rows which include inputs and buttons, namely. I would like to have a Popover display to the right of an input for a row if the value isn't matching the requirements defined. The button will also be disabled until the value of the input is correct.
Relevant HTML:
<div class="row col-md-4">
<table ng-controller="TestController" style="width: 100%">
<tr ng-repeat="element in model.InvoiceNumbers">
<td><input ng-model="element.id"
popover="Invoice must match ##-####!"
popover-placement="right"
popover-trigger="{{ { false: 'manual', true: 'blur'}[!isValidInvoice(element.id)] }}"
popover-title="{{element.id}}"/></td>
<td>{{element.id}}</td>
<td><button ng-disabled="!isValidInvoice(element.id)">Approve</button></td>
</tr>
</table>
</div>
Relevant JavaScript:
app.controller("TestController", function ($scope) {
$scope.model = {
InvoiceNumbers : [
{ id: '12-1234' },
{ id: '12-1235' },
{ id: '1234567' },
{ id: '1' },
{ id: '' }],
};
$scope.isValidInvoice = function (invoice) {
if (invoice == null) return false;
if (invoice.length != 7) return false;
if (invoice.search('[0-9]{2}-[0-9]{4}') == -1) return false;
return true;
};
});
The button gets disabled correctly on my local solution. However, I can't get the Popover to work; it behaves as if the model in its scope isn't getting updated. So, I looked through several links here (though most were from 2013 so I'd imagine a bit has changed) and their problems seemed to be solved by removing primitive binding. That didn't fix anything here. I added some console.log() lines in the function getting called from the Popover, and it was getting the correct value from the model each time. I also added a title to the Popover to show that its seeing the right value from the model.After seeing the log showing that it should be working correctly, I've run out of ideas.
The issue is element.id isn't updating dynamically within the trigger (it keeps its initial value, unlike popover-title which updates with the model). Is there something I did wrong?
Also, I've only been working with angular for a day so if you all have any suggestions on better ways to accomplish this, I'm open to suggestions.
Plunker: http://plnkr.co/edit/tiooSxSDgzXhbmIty3Kc?p=preview
Thanks
Found a solution on the angular-ui github page that involved adding these directives:
.directive( 'popPopup', function () {
return {
restrict: 'EA',
replace: true,
scope: { title: '#', content: '#', placement: '#', animation: '&', isOpen: '&' },
templateUrl: 'template/popover/popover.html'
};
})
.directive('pop', function($tooltip, $timeout) {
var tooltip = $tooltip('pop', 'pop', 'event');
var compile = angular.copy(tooltip.compile);
tooltip.compile = function (element, attrs) {
var parentCompile = compile(element, attrs);
return function(scope, element, attrs ) {
var first = true;
attrs.$observe('popShow', function (val) {
if (JSON.parse(!first || val || false)) {
$timeout(function () {
element.triggerHandler('event');
});
}
first = false;
});
parentCompile(scope, element, attrs);
}
};
return tooltip;
});
And here's the changes I made to the controller and view to make it work like I wanted in the original question:
<div class="row col-md-4">
<table ng-controller="TestController" style="width: 100%">
<tr ng-repeat="element in model.InvoiceNumbers">
<td><input ng-model="element.id"
pop="Invoice must match ##-####!"
pop-placement="right"
pop-show="{{element.showPop}}"
ng-blur="isValidInvoice($index, $event)" /></td>
<td>{{element.id}}</td>
<td><button ng-disabled="!isValidInvoice($index)">Approve</button></td>
</tr>
</table>
</div>
JavaScript:
app.controller("TestController", function ($scope) {
$scope.model = {
InvoiceNumbers: [
{ id: '12-1234', showPop: false },
{ id: '12-1235', showPop: false },
{ id: '1234567', showPop: false },
{ id: '1', showPop: false },
{ id: '', showPop: false }]
};
$scope.isValidInvoice = function ($index, $event) {
var obj = $scope.model.InvoiceNumbers[$index];
var isValid = function () {
if (obj.id === null) return false;
if (obj.id.length != 7) return false;
if (obj.id.search('[0-9]{2}-[0-9]{4}') == -1) return false;
return true;
};
if ($event != null && $event.type == "blur") obj.showPop = !isValid();
return isValid();
};
});
Plunker: http://plnkr.co/edit/5m6LHbapxp5jqk8jANR2?p=preview

how to exec js after AngularJS ng-repeat finished

I'm new to AngularJS. I want to use ng-repeat to render a list of data.
Each of the data should have a <abbr class="timeago" title="2012-10-10 05:47:21"></abbr> alike after rendered. And then I could use jquery plugin timeago to turn it into human friendly text about 1 hour ago.
My code is as below. But it take no effect. Please help.
EDIT: My problem is that, I can get the right html rendered. But code in directive do not run.
the html:
<div ng-app='weiboApp' ng-controller="WeiboListCtrl">
<table><tbody>
<tr ng-repeat='w in weibo' weiboLister='w'>
<td>{{ w.text }}></td>
<td><abbr class="timeago" title="{{ w.created }}"></abbr></td>
</tr>
</tbody></table>
</div>
the js:
var module = angular
.module('weiboApp', [])
.directive('weiboLister', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
scope.watch('w', function (val) {
console.log(element); //this never run
element.find("abbr.timeago").timeago(); //this never run
}, true);
}
}
});
function WeiboListCtrl($scope, $http) {
$http.get('/api/weibo/list').success(function(data) {
$scope.weibo = data;
});
}
The problem turned out to be: should define directive with camel-case weiboLister and use it in html with snake-case weibo-lister. Thanks to #tosh shimayama.
The correct code as below: (I added a remove function in case you're looking for the same thing.)
the html:
<div ng-app='weiboApp' ng-controller="WeiboListCtrl">
<table><tbody>
<tr ng-repeat='w in weibo' weibo-lister='w'> <!--important to be snake-case here-->
<td>{{ w.text }}></td>
<td><abbr class="timeago" title="{{ w.created }}"></abbr></td>
<td><a ng-click='remove(w)'>×</a></td>
</tr>
</tbody></table>
</div>
the js:
var module = angular
.module('weiboApp', [])
.directive('weiboLister', function () {
function delete(id, s_function, f_function) {
//...
if success { s_function(); }
else { f_function(); }
}
return {
restrict: 'A',
link: function (scope, element, attr) {
scope.$watch('w', function (val) {
element.find("abbr.timeago").timeago();
}
scope.destroy = function(callback) {
deletenews(scope.w.id, function () {
//s_function, things you want to do when delete with success
element.fadeOut(400, function () {
//this callback will splice the element in model
if (callback) callback.apply(scope);
})
}, function () {
//f_function, when delete with failure
});
};
}
}
});
function WeiboListCtrl($scope, $http) {
$http.get('/api/weibo/list').success(function(data) {
$scope.weibo = data;
});
$scope.removeWeibo = function(w) {
var idx = $scope.weibo.indexOf(w);
if (idx !== -1) {
this.destroy(function() {
$scope.weibo.splice(idx, 1);
});
}
};
}

Resources