Handling IE's clear button with AngularJS binding - angularjs

IE has an "X" in each text input that will clear the input. However, when clicking this button, while it clears the textbox, it does not update the Angular model that the input is bound to.
<input type="text" ng-model="name" />
See http://jsfiddle.net/p5x1zwr9/ for an example of the behavior.
See http://youtu.be/LFaEwliTzpQ for a video of the behavior.
I am using IE 11.
EDIT: There does seem to be a solution for Knockout, but I don't know how to apply it to AngularJS: Handle IE 9 & 10's clear button with Knockout binding
UPDATE: Jonathan Sampson helped me realize that this actually worked in AngularJS versions prior to 1.3.6 so this may be a new Angular bug.
UPDATE: Opened issue: https://github.com/angular/angular.js/issues/11193

The X button in input forms is native for IE10+ and you can`t do anything about it, but only hide it with CSS:
input[type=text]::-ms-clear {
display: none;
}
Then you can create your own directive to mimic this kind of behaviour. Just create a span, position it inside of an input and add ng-click to it, which will clear the model value of the input.

I created this Angular directive for input text elements, which manually calls the element's change() event when the clear ('X') button is clicked. This fixed the problem on our project. I hope it helps others.
angular.module('app')
.directive('input', function () {
return {
restrict: 'E',
scope: {},
link: function (scope, elem, attrs) {
// Only care about textboxes, not radio, checkbox, etc.
var validTypes = /^(search|email|url|tel|number|text)$/i;
if (!validTypes.test(attrs.type)) return;
// Bind to the mouseup event of the input textbox.
elem.bind('mouseup', function () {
// Get the old value (before click) and return if it's already empty
// as there's nothing to do.
var $input = $(this), oldValue = $input.val();
if (oldValue === '') return;
// Check new value after click, and if it's now empty it means the
// clear button was clicked. Manually trigger element's change() event.
setTimeout(function () {
var newValue = $input.val();
if (newValue === '') {
angular.element($input).change();
}
}, 1);
});
}
}
});
With thanks to this answer (Event fired when clearing text input on IE10 with clear icon) for the JavaScript code to detect the clear button click.

I was able to solve this using the following directive - derived from 0x783e's answer above. It may provide better compatibility with later versions of angular. It should work with $watches or parsers in addition to ng-change.
angular
.module('yourModuleName')
.directive('input', FixIEClearButton);
FixIEClearButton.$inject = ['$timeout', '$sniffer'];
function FixIEClearButton($timeout, $sniffer) {
var directive = {
restrict: 'E',
require: '?ngModel',
link: Link,
controller: function () { }
};
return directive;
function Link(scope, elem, attr, controller) {
var type = elem[0].type;
//ie11 doesn't seem to support the input event, at least according to angular
if (type !== 'text' || !controller || $sniffer.hasEvent('input')) {
return;
}
elem.on("mouseup", function (event) {
var oldValue = elem.val();
if (oldValue == "") {
return;
}
$timeout(function () {
var newValue = elem.val();
if (newValue !== oldValue) {
elem.val(oldValue);
elem.triggerHandler('keydown');
elem.val(newValue);
elem.triggerHandler('focus');
}
}, 0, false);
});
scope.$on('$destroy', destroy);
elem.on('$destroy', destroy);
function destroy() {
elem.off('mouseup');
}
}
}

While hiding using CSS
Instead of 'type=text' use 'type=search' in search fields.By doing this only inputs marked as 'type=search' will not have 'X' but other inputs will still have 'X' which is required on many other fields in IE.
input[type=search]::-ms-clear {
display: none;
}

<input type="text" ng-model="name" id="search" />
This solution works for me
$("#search").bind("mouseup", function(e){
var $input = $(this),
oldValue = $input.val();
if (oldValue == "") return;
// When this event is fired after clicking on the clear button
// the value is not cleared yet. We have to wait for it.
setTimeout(function(){
var newValue = $input.val();
if (newValue == ""){
$scope.name="";
$scope.$apply();
}
}, 1);
});

The solution I came up with, while doesn't update the model immediately like removing the X and implementing you own solution, It does solve for what i needed. All I did was add ng-model-options to include blur. So when the input is blurred it will update the scope value.
<input type="text" ng-model="name" ng-model-options="{ updateOn: 'default blur'}" />

Related

Open AngularUI Bootstrap Typeahead Matches Results via Controller

Is there anyway to trigger opening the match results on a typeahead input text box from the controller?
use case:
user goes to https://example.com/search/searchText
controller of page sets the input text to "searchText" (ng-model) on initialization
trigger showing the typeahead results from the controller
I can only seem to get the typeahead results, obviously, while typing in the input text box.
I got it to work in a couple ways, but both require changes to ui-bootstrap. I suppose I could create a pull request but not sure if my particular use case is a common one.
1) Custom directive and calling UibTypeaheadController.scheduleSearchWithTimeout method on focus of input element.
Directive:
.directive("showSearchResultsOnFocus", function($stateParams) {
return {
require: ['uibTypeahead', 'ngModel'],
link: function (scope, element, attr, ctrls) {
var typeaheadCtrl = ctrls[0];
var modelCtrl = ctrls[1];
element.bind('focus', function () {
if (!$stateParams.search || !modelCtrl.$viewValue) return;
typeaheadCtrl.exportScheduleSearchWithTimeout(modelCtrl.$viewValue);
});
}
}
Update to ui-bootstrap:
this.exportScheduleSearchWithTimeout = function(inputValue) {
return scheduleSearchWithTimeout(inputValue);
};
Bad: Requires making the method public on controller. Only method available is the init method and scope is isolated. Not meant to call from outside controller.
2) Add new typeahead attribute to allow setting default value and show results on focus:
Update to ui-bootstrap:
var isAllowedDefaultOnFocus = originalScope.$eval(attrs.typeaheadAllowDefaultOnFocus) !== false;
originalScope.$watch(attrs.typeaheadAllowedDefaultOnFocus, function (newVal) {
isAllowedDefaultOnFocus = newVal !== false;
});
element.bind('focus', function (evt) {
hasFocus = true;
// this was line before: if (minLength === 0 && !modelCtrl.$viewValue) {
if ((minLength === 0 && !modelCtrl.$viewValue) || isAllowedDefaultOnFocus) {
$timeout(function() {
getMatchesAsync(modelCtrl.$viewValue, evt);
}, 0);
}
});
Bad: Pull Request to ui-bootstrap but change perhaps not a common use feature. Submitted a PR here: https://github.com/angular-ui/bootstrap/pull/6353 Not sure if will be merged or not but using fork until then.
Any other suggestions?
Versions
Angular: 1.5.8, UIBS: 2.2.0, Bootstrap: 3.3.7

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

Angular ng-blur not working with ng-hide

Using a directive focus-me="inTextModeInput" in a text input
app.directive('focusMe', function($timeout) {
/*focuses on input
<input type="text" focus-me="focusInput">
*/
return {
scope: { trigger: '=focusMe' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === true) {
$timeout(function() {
element[0].focus();
scope.trigger = false;
});
}
});
}
};
});
Actually having 2 inputs, both uses focus-me
When i programatically set the value to focus on an input the ng-blur of other is not called.
NOTE : i am also using this in an ng-repeat.
Isolated scope
The blur is called, but you're not seeing that because you've created a directive with an isolated scope. The ng-blur is executed on the $parent scope. You should only use an isolated scope when the directive is implementing re-useable templates.
Two way binding on trigger
The line 'scope.trigger = false' is also setting a different boolean value because it's on a different scope. If you want to assign a value to a variable from a directive you should always wrap the value inside another object: var focus = { me: true } and set it like trigger=focus.me.
A better solution
But I wouldn't set the trigger to false at all. AngularJS is a MVC/MVVM based framework which has a model state for the user interface. This state should be idempotent; meaning that if you store the current state, reload the page and restore the state the user interface should be in the exact same situation as before.
So what you probably need is a directive that
Has no isolated scope (which allows all other directives to work: ng-blur, ng-focus, ...)
Keeps track of a boolean, which indicates the focus state
Sets this boolean to false when the element has lost focus
It's probably easier to see this thing in action: working plunker.
Maybe this (other) plunker will give you some more insight on scopes and directives.
Code
myApp.directive('myFocus', function($parse, $timeout) {
return {
restrict: 'A',
link: function myFocusLink($scope, $element, $attrs, ctrls) {
var e = $element[0];
// Grab a parser from the provided expression so we can
// read and assign a value to it.
var getModel = $parse($attrs.myFocus);
var setModel = getModel.assign;
// Watch the parser -- and focus if true or blur otherwise.
$scope.$watch(getModel, function(value) {
if(value) {
e.focus();
} else {
e.blur();
}
});
function onBlur() {
$timeout(function() {
setModel($scope, false);
});
}
function onFocus() {
$timeout(function() {
setModel($scope, true);
});
}
$element.on('focus', onFocus);
$element.on('blur', onBlur);
// Cleanup event registration if the scope is destroyed
$scope.$on('$destroy', function() {
$element.off('focus', onFocus);
$element.off('blur', onBlur);
});
}
};
});

Change focus to input on a key event in AngularJS

Test case: http://jsbin.com/ahugeg/4/edit (Slightly long)
In the above test case, I have three input elements, generated by ng-repeat directive. My intention in this test case, is that hitting the up/down arrows in one of these inputs should move focus to the input in the corresponding direction, if there is an input available in that direction.
I am new to AngularJS, so I might be missing on some straightforward way to do this. Anyway, I defined two new directives (on-up and on-down), to handle the up and down events and am calling the $scope.focusNext and $scope.focusPrev methods, passing the correct entry, relative to which the focus should move. This is where I am stuck.
I know it is not the angular-way to deal with DOM elements in controllers, but I can't see how the focus can be seen as an attribute/property of a model. I even thought of having a separate $scope.focusedEntry, but then should I watch for changes on that property? Even if I do and I detect changes, how can I access the input element corresponding to the entry I want focused?
Any help on how this should be done are very much appreciated.
I just wrote this up and tested it briefly - it does what you want without all the extra clutter in your controller and in the HTML. See it working here.
HTML:
<body ng-controller="Ctrl">
<input ng-repeat="entry in entries" value="{{entry}}" key-focus />
</body>
Controller:
function Ctrl($scope) {
$scope.entries = [ 'apple', 'ball', 'cow' ];
}
Directive:
app.directive('keyFocus', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.bind('keyup', function (e) {
// up arrow
if (e.keyCode == 38) {
if(!scope.$first) {
elem[0].previousElementSibling.focus();
}
}
// down arrow
else if (e.keyCode == 40) {
if(!scope.$last) {
elem[0].nextElementSibling.focus();
}
}
});
}
};
});
I had a similar problem and used this simple directive. It works as ng-show and ng-hide would- only with focus, if it's attribute resolves as true:
.directive('focusOn',function() {
return {
restrict : 'A',
link : function($scope,$element,$attr) {
$scope.$watch($attr.focusOn,function(focusVal) {
if(focusVal === true) {
setTimeout(function() {
$element.focus();
},50);
}
});
}
}
})
Inspired by #Trevor's solution, here's what I settled on,
app.directive('focusIter', function () {
return function (scope, elem, attrs) {
var atomSelector = attrs.focusIter;
elem.on('keyup', atomSelector, function (e) {
var atoms = elem.find(atomSelector),
toAtom = null;
for (var i = atoms.length - 1; i >= 0; i--) {
if (atoms[i] === e.target) {
if (e.keyCode === 38) {
toAtom = atoms[i - 1];
} else if (e.keyCode === 40) {
toAtom = atoms[i + 1];
}
break;
}
}
if (toAtom) toAtom.focus();
});
elem.on('keydown', atomSelector, function (e) {
if (e.keyCode === 38 || e.keyCode === 40)
e.preventDefault();
});
};
});
This defines an attribute focus-iter to be set on the parent element of all the repeated inputs. See this in action here: http://jsbin.com/ahugeg/10/.
The advantage over #Trevor's is that I can set an arbitrary selector for the value of focus-iter attribute to specify exactly which elements the focus jumping should work with. As a crazy example, try setting focus-iter attribute to input:even :). This helps since in my application, the inputs come with quite a bit of extra markup around them, unlike the test case.

Clear input text field in Angular / AngularUI with ESC key

In several places of my Angular app I need to clear inputs from user with the ESC key. The problem is, I don't know how to do it with text input fields (textarea is clearing OK). See this fiddle:
jsFiddle demonstration of the problem
Binding:
<input ng-model="search.query" ui-keypress="{esc: 'keyCallback($event)'}" />
Callback I use:
$scope.keyCallback = function($event) {
$event.preventDefault();
$scope.search.query = '';
}
Can anyone, please, figure out what I need to do to clear text input with ESC key?
SOLUTION:
As adviced by bmleite, you shouldn't listen for 'keypress' but for 'keydown' and 'keyup'. Problem was, that 'keydown' does not work in Firefox so only 'keyup' did the magic trick with listening for ESC. ;)
Working fiddle: http://jsfiddle.net/aGpNf/190/
SOLUTION UPDATE:
In the end I had to listen for both 'keydown' and 'keyup' events. Because in my case FF does reset input field on ESC keydown to previous state, so it messed up my model. So 'keyup' clears the model and 'keydown' checks if model is empty and does appropriate action. I also need to manually defocus input to prevent text popping back in. :/
The accepted answer does not work for IE 10/11. Here is a solution based on another question that does:
Directive
.directive('escKey', function () {
return function (scope, element, attrs) {
element.bind('keydown keypress', function (event) {
if(event.which === 27) { // 27 = esc key
scope.$apply(function (){
scope.$eval(attrs.escKey);
});
event.preventDefault();
}
});
scope.$on('$destroy', function() {
element.unbind('keydown keypress')
})
};
})
HTML:
<input ... ng-model="filter.abc" esc-key="resetFilter()" >
Ctrl
$scope.resetFilter = function() {
$scope.filter.abc = null;
};
I solve this problem like this (Controller as vm Syntax):
HTML
<input ... ng-model="vm.item" ng-keyup="vm.checkEvents($event)">
Controller
...
vm.checkEvents = function ($event) {
if ($event.keyCode == 27) {
vm.item = "";
}
}
Listen for 'keydown' or 'keyup' events instead of 'keypress':
<input ng-model="search.query" ui-keydown="{esc: 'keyCallback($event)'}" />
For now, with Angular v4, this works: (keyup.esc)="callback()"
I've managed to build a directive clearing directly ng-model of the input element and properly working also in Firefox. For that I need to check whether the value is already cleared (modelGetter(scope)) and also wrap the assignment to the zero $timeout method (to apply it in next digest call).
mod.directive('escClear', ['$timeout', '$parse', function($timeout, $parse) {
return {
link : function(scope, element, attributes, ctrl) {
var modelGetter = $parse(attributes.ngModel);
element.bind('keydown', function(e) {
if (e.keyCode === $.ui.keyCode.ESCAPE && modelGetter(scope)) {
$timeout(function() {
scope.$apply(function () {modelGetter.assign(scope, '');});
}, 0);
}
});
}
};
}]);
My $ property is jQuery, feel free to replace it with magic number 27.
Angular 2 version which also updates ngModel
Directive
import { Directive, Output, EventEmitter, ElementRef, HostListener } from '#angular/core';
#Directive({
selector: '[escapeInput]'
})
export class escapeInput {
#Output() ngModelChange: EventEmitter<any> = new EventEmitter();
private element: HTMLElement;
private KEY_ESCAPE: number = 27;
constructor(private elementRef: ElementRef) {
this.element = elementRef.nativeElement;
}
#HostListener('keyup', ['$event']) onKeyDown(event) {
if (event.keyCode == this.KEY_ESCAPE) {
event.target.value = '';
this.ngModelChange.emit(event.target.value);
}
}
}
Usage
<input escapeInput class="form-control" [(ngModel)]="modelValue" type="text" />

Resources