I created a custom directive to resolve some focus related issues in my application.
Directive Code:
(function() {
angular.module("FocusNextModule", []).directive("focusNext", function() {
return {
restrict: "A",
link: function($scope, elem, attrs) {
elem.bind("focus", function(e) {
var code = e.which || e.keyCode;
var nextElem = document.getElementById(attrs.focusNext);
if (nextElem === null || nextElem === undefined) {
var altElem = document.getElementById(attrs.focusNextAlt);
if (angular.element(altElem).hasClass('ng-hide') === false) {
altElem.focus();
} else {
var selfElem = document.getElementById(attrs.focusSelf);
selfElem.focus();
}
e.preventDefault();
} else {
nextElem.focus();
e.preventDefault();
}
});
}
};
});
})();
How to use in template Use InT emplate
<md-button id="idOfElementC">MyButton</md-button>
<div tabindex="0" focus-next="idOfElementA" focus-next-alt="idOfElementB" focus-self="idOfElementC"></div>
Note:
Element with "idOfElementC" id will be just above the div using focus-next directive.
How does the directive work?
When we press tab on element with "idOfElementC" id (here button), focus will go to div using focus-next directive. The div will redirect the focus to other elements using following cases:
a) First it will check if there is any element with id "idOfElementA". If element exists, then that element will receive focus.
b) If element with id "idOfElementA" do not exist, then "idOfElementB" will receive focus.
c) If element with id "idOfElementB" do not exist as well, then finally "idOfElementA" (on which tab was pressed) will receive focus.
The directive is working fine and fixing all my issues. But, I need to write jasmine test cases for this directive.
Can anyone guide me how to write Jasmine test cases for focus?
UPDATE:
As per comment of #PetrAveryanov the directive was looking horrible and I completely agree.
Updated Directive:
(function() {
angular.module("FocusNextModule", []).directive("focusNext", function() {
return {
restrict: "A",
link: function($scope, elem, attrs) {
elem.bind("focus", function(e) {
var elemToFocus = document.getElementById(attrs.focusNext) || document.getElementById(attrs.focusNextAlt);
/*jshint -W030 */
angular.element(elemToFocus).hasClass('ng-hide') === false ? elemToFocus.focus() : document.getElementById(attrs.focusSelf).focus();
e.preventDefault();
});
}
};
});
})();
Finally, got it how to write test cases for the directive.
describe('focus-next-directive test', function() {
var compile, scope;
beforeEach(module(FocusNextModule));
beforeEach(inject(function($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
}));
it('should focus the next element', function() {
var div = compile('<div tabindex="0" focus-next="idTestNext"/>')(scope);
var nextElem = compile('<input id="idTestNext" type="text" />')(scope);
angular.element(document.body).append(div);
angular.element(document.body).append(nextElem);
div.focus();
expect(nextElem).toEqual(angular.element(document.activeElement));
div.remove();
nextElem.remove();
});
it('should focus the next alternative element', function() {
var div = compile('<div tabindex="0" focus-next="idTestNext" focus-next-alt="idTestNextAlt"/>')(scope);
var nextAltElem = compile('<input id="idTestNextAlt" type="text" />')(scope);
angular.element(document.body).append(div);
angular.element(document.body).append(nextAltElem);
div.focus();
expect(nextAltElem).toEqual(angular.element(document.activeElement));
div.remove();
nextAltElem.remove();
});
it('should focus the Self element', function() {
var selfElem = compile('<input id="idTestSelf" type="text" ng-class="ng-hide"/>')(scope);
var div = compile('<div tabindex="0" focus-next="idTestNext" focus-next-alt="idTestNextAlt" focus-self="idTestSelf"/>')(scope);
var nextAltElem = compile('<input id="idTestNextAlt" type="text" class="ng-hide"/>')(scope);
angular.element(document.body).append(selfElem);
angular.element(document.body).append(div);
angular.element(document.body).append(nextAltElem);
div.focus();
expect(selfElem).toEqual(angular.element(document.activeElement));
div.remove();
selfElem.remove();
nextAltElem.remove();
});
});
Related
I want to connect a popup menu with multiple input elements and show the menu when a new input element is focused. The menu closes on an "outside-of-menu click".
simplified example plknr link / code below.
I'm wondering about what is the most direct way to update the position of the popup menu for this situation. In other words: How to get the info about the newly focused input element back into the directive to make the changes there (position and value of the input element).
In my code I'm storing info about the position on a service (and also the reference to the currently focused input element), but this is not working (the directive does not update without scope.$apply) .
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $document, eventService) {
$("input").on("focus", function(event) {
$scope.$apply(function() {
eventService.register(event.target, $scope);
eventService.positon = $(event.target).position();
$scope.position = eventService.position;
console.log("in", $scope.position);
eventService.addMenu();
});
console.log('from event', $(event.target).position());
});
$("input").on("blur", function() {
console.log('blured');
// eventService.closeList()
});
});
app.directive("myMenu", function($document, eventService, $compile) {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var menu = angular.element('<div id="menu" class="menu">menu {{menuText}}<div>');
$compile(menu)(scope);
eventService.input = $("input").first(); //set the first input
scope.menuText = eventService.input.val();
scope.$watch(function() {
return eventService.input.val();
}, function(newValue, ov) {
scope.menuText = newValue;
});
// $document.off("dialogmutex", closeMenu);
$document.on("dialogmutex", closeMenu);
// close menu on outside click:
$document.on("click", function(event) {
// if the menu or input is clicked dont close it.
if (!((event.target === elem[0]) || event.target === eventService.input[0] || (elem.find(event.target).length > 0))) {
$document.trigger("dialogmutex");
}
});
function addMenu() {
// positioning the menu does not work
var pos = eventService.position;
if (pos) {
elem.css({
top: pos.yPos,
left: pos.xPos,
position: 'absolute'
});
}
console.log("directive position:", pos);
elem.append(menu);
scope.menuText += " x "
}
function closeMenu() {
elem.find("#menu").remove();
}
addMenu(); // open menu on app start
eventService.addMenu = addMenu; // open the menu later from the controller via service
}
};
});
// service used to register a new input element with the directive.
app.service('eventService', function() {
service = {
register: function(el) {
service.input = $(el);
console.log('reg');
service.position = service.input.position();
console.log("on service", service.position)
}
};
return service;
});
Update:
I got it working using ngStyle directive on the container element and a positionCSS Object on the directive scope ,this way I only need to call $scope.$apply once (inside the event handler)
Using id for each input and the using .closest in jQuery should do the trick. You can refer to this link for detailed version.
I already have this code that I came up with:
In my outer controller:
$scope.key = function ($event) {
$scope.$broadcast('key', $event.keyCode)
}
In my inner controller (I have more than one like this)
$scope.$on('key', function (e, key) {
if (key == 13) {
if (ts.test.current) {
var btn = null;
if (ts.test.userTestId) {
btn = document.getElementById('viewQuestions');
} else {
btn = document.getElementById('acquireTest');
}
$timeout(function () {
btn.focus();
btn.click();
window.setTimeout(function () {
btn.blur();
}, 500);
})
}
}
});
Is there another way that I could simplify this using some features of AngularJS that I have not included here?
Please check this gist, https://gist.github.com/EpokK/5884263
You can simply create a directive ng-enter and pass your action as paramater
app.directive('ngEnter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
};
});
HTML
<input ng-enter="myControllerFunction()" />
You may change the name ng-enter to something different, because ng-** is a reserved by Angular core team.
Also I see that your controller is dealing with DOM, and you should not. Move those logic to other directive or to HTML, and keep your controller lean.
if (ts.test.userTestId) {
btn = document.getElementById('viewQuestions'); //NOT in controller
} else {
btn = document.getElementById('acquireTest'); //NOT in controller
}
$timeout(function () {
btn.focus(); //NOT in controller
btn.click(); //NOT in controller
window.setTimeout(function () { // $timeout in $timeout, questionable
btn.blur(); //NOT in controller
}, 500);
})
What i've done in the past is a directive which just listens for enter key inputs and then executes a function that is provided to it similar to an ng-click. This makes the logic stay in the controller, and will allow for reuse across multiple elements.
//directive
angular.module('yourModule').directive('enterHandler', [function () {
return{
restrict:'A',
link: function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
var key = event.which ? event.which : event.keyCode;
if (key === 13) {
scope.$apply(function () {
scope.$eval(attrs.enterHandler);
});
event.preventDefault();
}
});
}
}
}]);
then your controller becomes
$scope.eventHandler = function(){
if (ts.test.current) {
var btn = ts.test.userTestId
? document.getElementById('viewQuestions')
: document.getElementById('acquireTest');
$timeout(function () {
btn.focus();
btn.click();
window.setTimeout(function () {
btn.blur();
}, 500);
})
}
}
and your markup can then be
<div enter-handler="eventHandler()" ></div>
I have a directive containing a text field, and I want to test to make sure that the text entered into the field makes it to the model.
The directive:
define(function(require) {
'use strict';
var module = require('reporting/js/directives/app.directives');
var template = require('text!reporting/templates/text.box.tpl');
module.directive('textField', function () {
return {
restrict: 'A',
replace: true,
template:template,
scope: {
textField : "=",
textBoxResponses : "="
},
link: function(scope) {
scope.debug = function () {
scope;
// debugger;
};
}
};
});
return module;
});
The markup:
<div ng-form name="textBox">
<!-- <button ng-click="debug()">debug the text box button</button> -->
<h1>Text Box!</h1>
{{textField.label}} <input type="text" name="textBox" ng-model="textBoxResponses[textField.fieldName]">{{name}}
</div>
The test code:
/* global inject, expect, angular */
define(function(require){
'use strict';
require('angular');
require('angularMock');
require('reporting/js/directives/app.directives');
require('reporting/js/directives/text.box.directive');
describe("builder experimenter", function() {
var directive, scope;
beforeEach(module('app.directives'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
scope.textBoxResponses = {};
scope.textBoxField = {
fieldName : "textBox1"
};
directive = angular.element('<div text-field="textBoxField" text-box-responses="textBoxResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it('should put the text box value on the model', inject(function() {
directive.find(":text").val("something");
expect(scope.textBoxResponses.textBox1).toBe("something");
}));
});
});
So, what I'm trying to do in the last it block is to simulate typing in the text field, and then check to make sure that the new value of the text field makes it to the model. The issue is that the model is never updated with the new value.
The issue is ng-model is never informed that anything is in the textfield. ng-model is listening for the input event. All you have to do to fix your code is:
var text = directive.find(":text");
text.val("something");
text.trigger('input');
expect(scope.textBoxResponses.textBox1).toBe("something");
When the ng-model gets the event input, then check your scope and everything will be what you expect.
I got this done by using the sniffer service.
Your test will look like this:
var sniffer;
beforeEach(inject(function($compile, $rootScope, $sniffer) {
scope = $rootScope;
sniffer = $sniffer;
scope.textBoxResponses = {};
scope.textBoxField = {
fieldName : "textBox1"
};
directive = angular.element('<div text-field="textBoxField" text-box-responses="textBoxResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it('should put the text box value on the model', inject(function() {
directive.find(":text").val("something");
directive.find(":text").trigger(sniffer.hasEvent('input') ? 'input' : 'change');
expect(directive.isolateScope().textBoxResponses.textBox1).toBe("something");
}));
I found this pattern here: angular-ui-bootstrap typeahead test
The trigger basically makes the view value go into the model.
Hope this helps
I'm making a directive that modifies it's inner html. Code so far:
.directive('autotranslate', function($interpolate) {
return function(scope, element, attr) {
var html = element.html();
debugger;
html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
return '<span translate="' + text + '"></span>';
});
element.html(html);
}
})
It works, except that the inner html is not evaluated by angular. I want to trigger a revaluation of element's subtree. Is there a way to do that?
Thanks :)
You have to $compile your inner html like
.directive('autotranslate', function($interpolate, $compile) {
return function(scope, element, attr) {
var html = element.html();
debugger;
html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
return '<span translate="' + text + '"></span>';
});
element.html(html);
$compile(element.contents())(scope); //<---- recompilation
}
})
Here's a more generic method I developed to solve this problem:
angular.module('kcd.directives').directive('kcdRecompile', function($compile, $parse) {
'use strict';
return {
scope: true, // required to be able to clear watchers safely
compile: function(el) {
var template = getElementAsHtml(el);
return function link(scope, $el, attrs) {
var stopWatching = scope.$parent.$watch(attrs.kcdRecompile, function(_new, _old) {
var useBoolean = attrs.hasOwnProperty('useBoolean');
if ((useBoolean && (!_new || _new === 'false')) || (!useBoolean && (!_new || _new === _old))) {
return;
}
// reset kcdRecompile to false if we're using a boolean
if (useBoolean) {
$parse(attrs.kcdRecompile).assign(scope.$parent, false);
}
// recompile
var newEl = $compile(template)(scope.$parent);
$el.replaceWith(newEl);
// Destroy old scope, reassign new scope.
stopWatching();
scope.$destroy();
});
};
}
};
function getElementAsHtml(el) {
return angular.element('<a></a>').append(el.clone()).html();
}
});
You use it like so:
HTML
<div kcd-recompile="recompile.things" use-boolean>
<div ng-repeat="thing in ::things">
<img ng-src="{{::thing.getImage()}}">
<span>{{::thing.name}}</span>
</div>
</div>
JavaScript
$scope.recompile = { things: false };
$scope.$on('things.changed', function() { // or some other notification mechanism that you need to recompile...
$scope.recompile.things = true;
});
Edit
If you're looking at this, I would seriously recommend looking at the website's version as that is likely to be more up to date.
This turned out to work even better than #Reza's solution
.directive('autotranslate', function() {
return {
compile: function(element, attrs) {
var html = element.html();
html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
return '<span translate="' + text + '"></span>';
});
element.html(html);
}
};
})
Reza's code work when scope is the scope for all of it child elements. However, if there's an ng-controller or something in one of the childnodes of this directive, the scope variables aren't found. However, with this solution ^, it just works!
I'm new to Angular, and I'm trying to get the XY coordinates of a tap using angular-hammer.js directives. Here's how the directives are set up:
var hmTouchevents = angular.module('hmTouchevents', []),
hmGestures = ['hmHold:hold',
'hmTap:tap',
'hmDoubletap:doubletap',
'hmDrag:drag',
'hmDragup:dragup',
'hmDragdown:dragdown',
'hmDragleft:dragleft',
'hmDragright:dragright',
'hmSwipe:swipe',
'hmSwipeup:swipeup',
'hmSwipedown:swipedown',
'hmSwipeleft:swipeleft',
'hmSwiperight:swiperight',
'hmTransform:transform',
'hmRotate:rotate',
'hmPinch:pinch',
'hmPinchin:pinchin',
'hmPinchout:pinchout',
'hmTouch:touch',
'hmRelease:release'];
angular.forEach(hmGestures, function(name){
var directive = name.split(':'),
directiveName = directive[0],
eventName = directive[1];
hmTouchevents.directive(directiveName, ["$parse", function($parse) {
return {
scope: true,
link: function(scope, element, attr) {
var fn, opts;
fn = $parse(attr[directiveName]);
opts = $parse(attr["hmOptions"])(scope, {});
scope.hammer = scope.hammer || Hammer(element[0], opts);
return scope.hammer.on(eventName, function(event) {
return scope.$apply(function() {
return fn(scope, {
$event: event
});
});
});
}
};
}
]);
});
My html looks like this:
<div ng-controller="IndexCtrl" >
<div class='tap-area' hm-tap="tap();">
</div>
</div>
My controller looks like this:
App.controller('IndexCtrl', function ($scope, Myapp) {
$scope.tap = function(ev){
//How do I get the event.gesture.center.pageX in here?
};
});
I figured out how to make this work. After return scope.hammer.on(eventName, function(event) { I added scope.event = event; and then in my controller I can get XY coords of a tap by using this.event.center.pageX or this.event.center.pageY.
It was posted long time ago but here is another solution.
Just add $event to your html