AngularJS $watch is called on every keypress in a text input - angularjs

I have a webpage where i am building a form from dynamic json data received from server. I have developed it with KnockoutJS, a while ago. After building the form, i also have a requirement to update the form by doing an ajax request from the server, everytime a value in the form has been changed. Here is the fiddle
You see that, change event only occurs on blur (not on keypress). For example, if a text field has a value 15, and user presses backspace and then enters 5 again, that means 2 keypresses but value has not been changed. So far so good.
Problem 1
Now i am converting the code to AngularJS. I am trying to catch the change event by $watch of scope. But, it seems like watch is run everytime a keypress is happened, even though after few keypresses value is not changed. Here is the fiddle of how i am attempting. This problem is however with only text type input fields.
Problem 2
I am creating radiogroup in a manual way, e.g. if the json data is changed then we will also have to update the html as well. How can i do that in a kind of dynamic way. I was able to do that with KnockoutJS.
Problem 3
Why ng-hide is not working?

Fix for the problem 1 was to introduce a directive for blur event, following is the code of my directive:
angular.module('app', []).directive('ngModelOnblur', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function () {
scope.$apply(function () {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
you can see the working fiddle here http://jsfiddle.net/akeelrehman/GNJtn/1/
I couldn't figure out the solution for problem 2, so i have stopped using radio inputs.

Instead of using blur, you could just use a timeout. In effect this calls your function when the user has stopped typing for X milliseconds, instead of on every keypress.
See this reply to a similar question, which has example code that does the job: https://stackoverflow.com/a/15723514/1034002

Related

How to write a logic when double clicking on page using angularjs

I have a page in which I have a few elements. My scenario is, when I double click on the background i.e., not clicking on any of the elements. I need to go 1 page back using angular. I am pretty new to angular. I am trying to figure out a way to implement. Any suggestions/help is appreciated.
Javascript has a native dblclick event, which is described here.
You'll need to check that the event is coming from the body itself, instead of one of the elements generating an event that then bubbles up to the body.
Try something like this, passing the body as the element:
link: function(scope, element, attrs) {
element.on('dblclick', function(e) {
if(e.target === element){
$window.history.back();
}
});
}

UI Bootstrap control uib-carousel bind to next event

I am using the angular-bootstrap's (ui.bootstrap) uib-carousel component with custom template to change the style, text, and functionality of the back and next buttons. I have to perform other actions when the "next" button is touched, in addition to the component's original functionality. Following the method of "capturing" the select event in this answer: How do you Bind to Angular-UI's Carousel Slide Events?, I modified the answer to use the next event.
the html declaration
<uib-carousel template-url="/tpl.html" active="vm.wizardStep" no-wrap="true" on-carousel-next="vm.onNext()" style="height: 395px;">
and the directive like so
.directive('onCarouselNext', function($parse) {
return {
require: '^uibCarousel',
link: function(scope, element, attrs, carouselCtrl) {
var callBack = $parse(attrs.onCarouselNext);
var origNext = carouselCtrl.next;
carouselCtrl.next = function() {
callBack(scope);
return origNext.apply(this, arguments);
};
}
};
});
The uib-carousel's next event does get fired, but it is not calling it through my directive. Just to compare apples to apples, I tried using the code in the answer as-is (i.e. capturing the "select" event), and that does work perfectly and calls my callback function. The reason I need to capture the "next" and can't use the "select" is because I'm using this to set up a "wizard" type framework, and the final "next" is a "done" that needs to do different code.
Anyone done something like this and get it to work?
So seeing as how I was trying to "hijack" the carousel control to turn it in to a wizard control, I ended up just taking the carousel code and changing it completely to fit my needs. If anyone would like to see the code, let me know and I'll put it out on github.

focus on last input ng-repeat

I have been looking at a lot of focus directives, especially those posted here.
But none seem to work for my scenario. I have a tabset and a table inside with an ng-repeat that creates rows with inputs on it. The way I create them is using ng-blur at the end of the last row checkbox so the ng-blur fires the addNewRow method. What I want is to focus the first input on the newly created row but the focus goes to the browser search bar instead.
I created a fiddle to show the problem using a directive from the first posted link.
I tested this directive:
soccerApp.directive('setfocus', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var focus = !! attrs.setfocus && !attrs.setfocus.replace(/true/, '');
if (focus === true) {
//alert(element);
element[0].focus();
}
}
};
});
and the element[0].focus(); actually fires when I create a new row but doesn't set the focus.
Can someone please take a look and tell me what could be wrong?
NOTE: The idea is to use the TAB key to loop through the inputs until the last one.
As Deblaton Jean-Philippe pointed out
The problem is that, at the moment you "tab" from the last input, your browser sets the focus out of the page. So you don't have any control on it anymore. I don't know if it's possible to override that without creating a fake invisible input below to keep the focus into the page
i was missing a fake input so i added a fake at the bottom after my table and it worked

AngularJS: Attempt to dynamically apply directive using ngClass causing weird functional and performance issues

Requirement
I want a textarea that expands or contracts vertically as the user types, alla Facebook comment box.
When the textarea loses focus it contracts to one line (with ellipsis if content overflows) and re-expands to the size of the entered text upon re-focus (this functionality not found on Facebook)
Note: Clicking on the textarea should preserve caret position exactly where user clicked, which precludes any dynamic swapping of div for textarea as the control receives focus
Attempted Solution
I'm already well into an AngularJS implementation, so...
Use Monospaced's Angular Elastic plugin. Nice.
Two attempts...
Attempt A: <textarea ng-focus="isFocussed=true" ng-blur="isFocussed=false" ng-class="'msd-elastic': isFocussed"></textarea> Fails because ng-class triggers no re-$compile of the element after adding the class, so Angular Elastic is never invoked
Attempt B: create a custom directive that does the needed re-$compile upon class add. I used this solution by hassassin. Fails with the following problems
Attempt B problems
Here's a JSFiddle of Attempt B Note that Angular v1.2.15 is used
I. Disappearing text
go to the fiddle
type into one textarea
blur focus on that textarea (eg click in the other textarea)
focus back on the text-containing textarea
result: text disappears! (not expected or desired)
II. Increasingly excessive looping and eventual browser meltdown
click into one textarea
click into the other one
repeat the above for as long as you can until the browser stops responding and you get CPU 100% or unresponsive script warnings.
you'll notice that it starts out OK, but gets worse the more you click
I confirmed this using: XP/Firefox v27, XP/Chrome v33, Win7/Chrome v33
My investigations so far
It seems that traverseScopesLoop in AngularJS starting at line 12012 of v1.2.15, gets out of control, looping hundreds of times. I put a console.log just under the do { // "traverse the scopes" loop line and clocked thousands of loops just clicking.
Curiously, I don't get the same problems in Angular v1.0.4. See this JSFiddle which is identical, except for Angular version
I logged this as an AngularJS bug and it was closed immediately, because I'd not shown it to be a bug in Angular per se.
Questions
Is there another way to solve this to avoid the pattern in Attempt B?
Is there a better way to implement Attempt B? There's no activity on that stackoverflow issue after hassassin offered the solution I used
Are the problems with Attempt B in my code, angular-elastic, hassassin's code, or my implementation of it all? Appreciate tips on how to debug so I can "fish for myself" in future. I've been using Chrome debug and Batarang for a half day already without much success.
Does it seem sensible to make a feature request to the AngularJS team for the pattern in Attempt A? Since you can add directives by class in Angular, and ngClass can add classes dynamically, it seems only natural to solve the problem this way.
You are severely overthinking this, and I can't think of any reason you would ever need to dynamically add / remove a directive, you could just as easily, inside the directive, check if it should do anything. All you need to do is
Use the elastic plugin you are using
Use your own directive to reset height / add ellipsis when it doesn't have focus.
So something like this will work (not pretty, but just threw it together):
http://jsfiddle.net/ss6Y5/8/
angular.module("App", ['monospaced.elastic']).directive('dynamicClass', function($compile) {
return {
scope: { ngModel: '=' },
require: "?ngModel",
link: function(scope, elt, attrs, ngModel) {
var tmpModel = false;
var origHeight = elt.css('height');
var height = elt.css('height');
var heightChangeIndex = 0;
scope.$watch('ngModel', function() {
if (elt.css('height') > origHeight && !heightChangeIndex) {
heightChangeIndex = scope.ngModel.length;
console.log(heightChangeIndex);
}
else if (elt.css('height') <= origHeight && elt.is(':focus')) {
heightChangeIndex = 0;
}
});
elt.on('blur focus', function() {
var tmp = elt.css('height');
elt.css('height', height);
height = tmp;
});
elt.on('blur', function() {
if (height > origHeight) {
tmpModel = angular.copy(scope.ngModel);
ngModel.$setViewValue(scope.ngModel.substr(0, heightChangeIndex-4) + '...');
ngModel.$render();
}
});
elt.on('focus', function() {
if (tmpModel.length) {
scope.ngModel = tmpModel;
ngModel.$setViewValue(scope.ngModel);
ngModel.$render();
tmpModel = '';
}
});
}
};
})

Adding Scope to Directive kills model relation

The Goal: To provide a series of text fields where when you have it selected and press the enter key it will either add a new text field (if you press enter in the last field) or focus on the next field (if you press enter on any other field).
The Idea: Using a directive that detects enter key presses will trigger a callback to the original controller that will be able to perform the necessary tasks. Ideally this directive will be able to use elsewhere for other actions other than the next / new text fields goal stated above.
The Problem: I've written the directive that will detect the enter key press and will then make a call to the callback function WITH the event data (used to detect which field was active when pressing the enter key). This works great! However... when the scope: [method: '&ngEnter'] is on the directive it kills all relationship between the text field and the controller (??). Therefore all textfields are blank (yet the other sub data found elsewhere (anywhere but the input) function properly, so the data itself is intact and correct, but when applied to the textfield it acts all screwy.
The Code: Found Below...
Directive: ng_enter.coffee
angular.module('app.common').directive "ngEnter", ['$parse', ($parse) ->
restrict: 'A'
scope:
method: '&ngEnter'
link: (scope, element, attrs) ->
expressionHandler = scope.method()
element.bind "keydown keypress", (event) ->
if event.which is 13
event.preventDefault()
scope.$apply ->
expressionHandler(event)
]
my_view.haml
.item-list.sub-item-list.bordered-background
.item-row(ng-repeat='sub in categoryEditor.categorizations' ng-hide='sub._destroy')
%div
%input.left{type: 'text', "ng-model"=>"sub.name", style: 'width: 85%; margin: 0 0 0 1px;', 'ng-enter' => 'subitemEnter'}
%i.icon-large.right.icon-remove{"ng-click"=>"removeSubItem(sub)", title: "{{sub.name}}"}
my_controller.coffee
angular.module('app.admin.controllers').controller 'MyControllerCtrl', ['$scope', ($scope) ->
$scope.categoryEditor = {...}
$scope.subitemEnter = (event) ->
console.log "subitemEnter() Triggered"
console.log event
Update: It appears that scope is being overwritten, and i must find a way to solve this without explicitly declaring scope in my directive.
Installing Angular UI Utils, I was able to get access to ui-keypress that provides a easy way of performing callbacks on various keypress events. The following code is the solution.
View (.haml):
%input{type: 'text', "ng-model"=>"sub.name", 'ui-keypress' => "{13:'subItemEnterPress($event)'}"}
Controller (.coffee):
$scope.subItemEnterPress = (event) ->
event.preventDefault()
inputs = $(event.target).closest('.item-list').find('input')
if event.target == _.last(inputs)
$scope.addSubItem()
else
$(inputs[ _.indexOf(inputs, event.target) + 1 ]).focus()

Resources