Angular.js keypress events and factories - angularjs

I'm building an app in Angular and I'd like to have keypress events.
However, I'd prefer not to litter my code with a keypress here and a keypress there, but rather to put all keypress events into a single factory (or service) and then import this factory into my controllers to use.
I HOPE that doing things this way will make it easier for me to manage the keypress events and make sure I don't have conflicts (two events tied to the same keypresses) or something like that.
Does anybody have any suggestions on how to manage this?
I'm using angular-ui-keypress.
As an example of how I'm hoping to use keypress events.
The user may have multiple tabs open and hits 'cmd+s' to save the files.
Rather than a 'save' method being triggered on each of the open files, I've got an OpenFilesFactory, the keypress would map to the OpenFilesFactory.saveFiles method.
Am I going about this all wrong? Is there a reason not to tie keyboard shortcuts to a factory rather than in a controller?

What I ended up doing worked surprisingly well, and I'll opensource it as a module after a bit more work.
I created a directive, which binds to the keypress events on $document
angular.module('keypress', []).directive('keypressEvents',
function($document, $rootScope) {
return {
restrict: 'A',
link: function() {
$document.bind('keypress', function(e) {
$rootScope.$broadcast('keypress',e , String.fromCharCode(e.which));
});
}
}
})
I then created a second directive for watching for keypresses on specific elements, basically giving the element a focus for key events.
angular.module('focus', []).directive('onFocus',
function() {
return {
restrict: 'C',
link: function(scope) {
scope.$on('keypress',function(e,parent_evt,key){
if(scope.keyBindings[key]){
scope.keyBindings[key](parent_evt, e);
// params reversed so user goes up the chain
}
});
}
}
});
In any controller where you want to use keyboard shortcuts, add a keybindings object
function keyedS(key, parent_evt, evt){
// key is the key that was pressed
// parent_evt is the keypress event
// evt is the focused element object
}
$scope.keyBindings = {
's': keyedS
}
Feedback?
I've actually put this together with multiple keybindings, so if the user selects 'ctrl-shift-s', that is what gets passed along the chain. Though I'm still struggling to find a really good way of getting all the press events. Eg. Tab doesn't work at the moment.

I get what you mean by having it be a separate resource. To me, it seems to go against thinking in Angular, as events really should be "controlled." If you want to have all keypress (or click) events centralized, maybe a switch/case is in order:
$scope.keypressHandler = function () {
switch ($event.keyCode)
{
case 13:
$scope.someEnterKeyFunction();
break;
default:
$scope.someDefaultFunction();
break;
}
}

Related

Targeting keydown events to an angularJS custom directive

I am working on a problem wherein I am required to pick-up keydown events (specifically ctrl+p and then point to a print function which already exists) on a certain custom directive and under certain conditions (a certain tab should be selected). My current approach is to bind the keydown event on the document itself, broadcast it and then listen to it in the required custom directive. Following is the code I have placed in the app.run.. block -
angular.element($document).on('keydown', function(evt) {
if(evt.ctrlKey && evt.key==='p'){
$rootScope.$broadcast('printOnKeyPress');
}
});
This part is working as expected, the problem arises when I try to handle it in the required controller of the custom directive as follows:
$scope.$on('printOnKeyPress', function() {
//point to existing print function
}
This is where the problem arises. It goes into the print function but still the output is incorrect. I am missing something and I can't figure out what.
Also, this is not a good approach but I have searched and am unable to find a possible solution to just bind the keydown event on that custom directive itself (the component only appears if a document is selected).
(ng-keydown won't also work here)
Any help is appreciated!
You could put it in the directive and use the scope destroy event to remove the listener.
Within directive link function:
function keyHandler(evt) {
if (evt.ctrlKey && evt.key === 'p') {
// do your print
}
}
angular.element($document).on('keydown', keyHandler);
scope.$on('$destroy', function() {
angular.element($document).off('keydown', keyHandler);
});

mdbottomsheet disable drag down to close

I would like to disable the drag down to close gesture of mdbottomsheet. I've found a work around on scripts but I'm not sure where to put the code. Thanks for the help.
As you say that angular-material doesn't provide any option to disable it, obviously you will have to make changes in its source code.
Now, you haven't mentioned whether you want to disable it at specific places or turn drag-down-to-close for bottomSheets everywhere.
1) In case of latter, it would be quite straightforward, as the only thing you need to do is remove the event listeners for drag events.
If you use angular-material.js file, heres what you can do:
Find the function BottomSheet(element, parent). This function basically registers the drag events which close the sheet. We need make it not attach the listeners.
Reduce it to:
function BottomSheet(element, parent){
return {
element: element,
cleanup: angular.noop
};
}
The cleanup function basically de-registers the listeners on drag event.This function is called when the scope of the bottomSheet is destroyed. To make minimal changes, we have just reduced the cleanup function to do nothing.
2) If you want to be able to pass an option while creating the sheet in your controller, you do the same thing, but conditionally based on the option you pass. Wont write the code because I assume you know how angular works, but here are the steps:
=> Add a boolean variable along with other options(template,scope,etc. ). Lets call it dragDownToClose.
=> In the defaults injector function inside the provider function of MdbottomSheet , assign it a default value (true/false).
=>Pass this along with element and parent during instantiation of BottomSheet() inside the onShow function.
=> So BottomSheet() will now have three argument - dragDownToClose being the new one.
=> As we did in the former case, return the element without any handler attached when the value is false, and let the original function be when its true.
Of-course there are various ways in which you can actually implement this. However, I hope you get the idea.
First, inject $element into your controller. You known what AngularJS $element do, right?
Then we both known that the drag events are registered in BottomSheet
parent.on('$md.dragstart', onDragStart)
.on('$md.drag', onDrag)
.on('$md.dragend', onDragEnd);
So, the simple solution is: Remove those events, override those events... without override the function BottomSheet, right?
$element
.on('$md.dragstart', function (ev) {
return false;
})
.on('$md.drag', function (ev) {
return false;
})
.on('$md.dragend', function (ev) {
return false;
});
Something still wrong here! The backdrop still draggable! So, we do the same for backdrop
var parent = $element.parent();
var backdrop = parent.find('md-backdrop');
backdrop
.on(blah blah blah
These is code in case you are asking for
You can try
$mdBottomSheet.show({
template: *yourTemplate*,
clickOutsideToClose:false
})
this will not let the user close the bottom sheet even with drag or click outside.

How to know the source of the event in Angularjs

I am looking for a way to know the source of the event in angularjs routing. Here is the scenario I am trying to solve. When url changes I want to know whether the change was caused by a browser back button or not. I thought I can do like this
$scope.$on('$routeUpdate', function (event) {
//if source is back button do this stuff
// if not do this
});
But the event object doesn't have a source information. Did I miss something here? Is there a better way to do it?
Instead of $routeUpdate use '$locationChangeStart'
Here's the discussion about it from the angularjs guys: https://github.com/angular/angular.js/issues/2109
Example:
$scope.$on('$locationChangeStart', function(event) {
if ($scope.form.$invalid) {
event.preventDefault();
}
});

How do you set the focus to a control in an angular app?

The question stems from an ng-grid interaction, but I've stubbed my toe on it a few other places.
An interaction with the page raises the need for the focus to be on a certain control.
For ng-grid this might be clicking on the filter button in the header. This causes a popup (but not really a modal dialog) input control to appear which then needs the focus, so the user doesn't then have to click a second time to enter the filter text. I'm assuming this should be done in the directive, but how do you get that directive at the point this is happening?
Another interaction might be after an attempted save on a form. Let say there is a validation that can't happen in the client, (multi-user app, race condition to acquire a resource). When the error returns from the promise, I'd like to put the cursor in the field that needs to be changed. )Which field depends on the error response from the server.)
I think what I'm really looking for is the equivalent of an $('#id').focus() that accepts an ng-model and finds the correct control on the page and puts the cursor in that field, but one that could be used at the completion of promise or in reaction to an event. I realize that linkage from model => DOM is problematic (a model could appear many places on the page), but in the case of form input, that probably isn't true, or could be made not-true to facilitate this kind of application response.
I'm a little lost as to where the logic should be and how I could get hold of the object that would contain it.
There's a similar question that has some good answers. I think the second answer comes closest to what you're asking for
How to set focus on input field?
Instead of binding a boolean "should be focused" field, the directive uses events to set focus on the desired element.
There are several advantages to this approach over the $('#id').focus() functionality you were describing. The biggest one is that with the event approach, your controller can be mocked and tested outside the DOM, because the tests can just look for fired events, rather than checking which DOM element has focus.
Here's a really generic example of how you might do it for field validation. Kudos to #blesh for the directive/service implementations
In your form you can add the focus-on directive. Just use a unique event name for each field:
<input type="text" name="name" focus-on="input-name" />
<input type="text" name="email" focus-on="input-email" />
In your controller, after you do field validation, you can call the "focus" service on the using the focus-on="input-{name}" value you specified in your template. Here I just prepend input- to the field name, but you could use any naming convention you like.
app.controller('MyCtrl', function($scope, focus) {
$scope.validate = function() {
// ...
// Do your field validation
// ...
if (invalidFields.length) {
focus('input-' + invalidFields[0].name);
}
};
});
Then just define your directive and service
app.directive('focusOn', function() {
return function(scope, elem, attr) {
scope.$on('focusOn', function(e, name) {
if(name === attr.focusOn) {
elem[0].focus();
}
});
};
});
app.factory('focus', function ($rootScope, $timeout) {
return function(name) {
$timeout(function (){
$rootScope.$broadcast('focusOn', name);
});
}
});

Set focus when another element is clicked or other event occurs

I have a tab-like view on my page and I have a variety of events which need to set the focus to one of the tabs. (showing/hiding the div is easy since I just use a model variable.) The events that cause a div to be focused are clicking on the tab header, data load completion, and initial loading. I know where to intercept all these events, but I'm not sure how I tell the other element to set the focus (none of the intercept sites know about the other element, only about the model).
I've looked around but can't find a good reference for this. I assume I want to listen for some message and post it from the various other locations.
How does one setup this type of messaging event?
ANSWER: I built on the answer and came up with the below directive. It combines both a focus and show state for the div.
newsendApp.directive('showAndFocus', function() {
return {
link: function(scope, element, attr){
scope.$on('SetArticlesListFocus',function() {
if( scope.$eval( attr.showAndFocus ) ) {
$(element).focus();
}
})
scope.$watch(attr.showAndFocus, function(value) {
if( value ) {
$(element).show();
setTimeout( function() { $(element).focus(); }, 0 );
} else {
$(element).hide();
}
})
}
}
});
If an event occurs which may require resetting the focus then I do: $rootScope.$broadcast( 'SetArticlesListFocus' );
You can broadcast an event, and also have listeners that perform an action once such a broadcast has occured.
You can use $scope.$broadcast('changed-tab', objectSentWithBroadcast). The second parameter is an object you can optionally send, like the tab you want to focus on.
Then you would have a listener like this where you can select the tab wanted:
$scope.$on('changed-tab', function(e, objectSentWithBroadcast) {
// do something here to select the tab
});
Have a look at this issue raised in angularjs.
https://github.com/angular/angular.js/issues/1277#issuecomment-16012024
Here is the final plunk that creates a ng-focus directive that manages two way data binding between a variable and an element's focus state.
http://plnkr.co/edit/bntEsfngnJKuneg2raD1
This will allow you to bind a model to an elements focus state and then setting or unsetting this variable will make the element gain / lose focus. You will have to note that if you are using div's etc ( which are non-focusable by default! ) you will need to set tabIndex = -1 on them. Apart from that it should work fine and suit your need for manipulating models pretty well.

Resources