AngularJS directives: ng-click is not triggered after blur - angularjs

DEMO
Consider the following example:
<input type="text" ng-model="client.phoneNumber" phone-number>
<button ng-click="doSomething()">Do Something</button>
.directive("phoneNumber", function($compile) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.mobileNumberIsValid = true;
var errorTemplate = "<span ng-show='!mobileNumberIsValid'>Error</span>";
element.after($compile(errorTemplate)(scope)).on('blur', function() {
scope.$apply(function() {
scope.mobileNumberIsValid = /^\d*$/.test(element.val());
});
});
}
};
});
Looking at the demo, if you add say 'a' at the end of the phone number, and click the button, doSomething() is not called. If you click the button again, then doSomething() is called.
Why doSomething() is not called for the first time? Any ideas how to fix this?
Note: It is important to keep the validation on blur.

Explain
Use click button, mousedown event is triggered on button element.
Input is on blur, blur callback triggered to validate input value.
If invalid, error span is displayed, pushing button tag down, thus cursor left button area. If user release mouse, mouseup event is not triggered. This acts like click on a button but move outside of it before releasing mouse to cancel the button click. This is the reason ng-click is not triggered. Because mouseup event is not triggered on button.
Solution
Use ng-pattern to dynamically validate the input value, and show/hide error span immediately according to ngModel.$invalid property.
Demo 1 http://jsbin.com/epEBECAy/14/edit
----- Update 1 -----
According to author's request, updated answer with another solution.
Demo 2 http://jsbin.com/epEBECAy/21/edit?html,js
HTML
<body ng-app="Demo" ng-controller="DemoCtrl as demoCtrl">
<pre>{{ client | json }}</pre>
<div id="wrapper">
<input type="text" phone-number
ng-model="client.phoneNumber"
ng-blur="demoCtrl.validateInput(client.phoneNumber)">
</div>
<button ng-mousedown="demoCtrl.pauseValidation()"
ng-mouseup="demoCtrl.resumeValidation()"
ng-click="doSomething()">Do Something</button>
</body>
Logic
I used ng-blur directive on input to trigger validation. If ng-mousedown is triggered before ng-blur, ng-blur callback will be deferred until ng-mouseup is fired. This is accomplished by utilizing $q service.

Here: http://jsbin.com/epEBECAy/25/edit
As explained by other answers, the button is moved by the appearance of the span before an onmouseup event on the button occurs, thus causing the issue you are experiencing. The easiest way to accomplish what you want is to style the form in such a way that the appearance of the span does not cause the button to move (this is a good idea in general from a UX perspective). You can do this by wrapping the input element in a div with a style of white-space:nowrap. As long as there is enough horizontal space for the span, the button will not move and the ng-click event will work as expected.
<div id="wrapper">
<div style='white-space:nowrap;'>
<input type="text" ng-model="client.phoneNumber" phone-number>
</div>
<button ng-click="doSomething()">Do Something</button>
</div>

It is because the directive is inserting the <span>Error</span> underneath where the button is currently placed, interfering with the click event location. You can see this by moving the button above the text box, and everything should work fine.
EDIT:
If you really must have the error in the same position, and solve the issue without creating your own click directive, you can use ng-mousedown instead of ng-click. This will trigger the click code before handling the blur event.

Not a direct answer, but a suggestion for writing the directive differently (the html is the same):
http://jsbin.com/OTELeFe/1/
angular.module("Demo", [])
.controller("DemoCtrl", function($scope) {
$scope.client = {
phoneNumber: '0451785986'
};
$scope.doSomething = function() {
console.log('Doing...');
};
})
.directive("phoneNumber", function($compile) {
var errorTemplate = "<span ng-show='!mobileNumberIsValid'> Error </span>";
var link = function(scope, element, attrs) {
$compile(element.find("span"))(scope);
scope.mobileNumberIsValid = true;
scope.$watch('ngModel', function(v){
scope.mobileNumberIsValid = /^\d*$/.test(v);
});
};
var compile = function(element, attrs){
var h = element[0].outerHTML;
var newHtml = [
'<div>',
h.replace('phone-number', ''),
errorTemplate,
'</div>'
].join("\n");
element.replaceWith(newHtml);
return link;
};
return {
scope: {
ngModel: '='
},
compile: compile
};
});

I would suggest using $parsers and $setValidity way while validating phone number.
app.directive('phoneNumber', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ctrl) {
if (!ctrl) return;
ctrl.$parsers.unshift(function(viewValue) {
var valid = /^\d*$/.test(viewValue);
ctrl.$setValidity('phoneNumber', valid);
return viewValue;
});
ctrl.$formatters.unshift(function(modelValue) {
var valid = /^\d*$/.test(modelValue);
ctrl.$setValidity('phoneNumber', valid);
return modelValue;
});
}
}
});
So, you will be able to use $valid property on a field in your view:
<form name="form" ng-submit="doSomething()" novalidate>
<input type="text" name="phone" ng-model="phoneNumber" phone-number/>
<p ng-show="form.phone.$invalid">(Show on error)Wrong phone number</p>
</form>
If you want to show errors only on blur you can use (found here: AngularJS Forms - Validate Fields After User Has Left Field):
var focusDirective = function () {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attrs, ctrl) {
var elm = $(element);
if (!ctrl) return;
elm.on('focus', function () {
elm.addClass('has-focus');
ctrl.$hasFocus = true;
if(!scope.$$phase) scope.$digest();
});
elm.on('blur', function () {
elm.removeClass('has-focus');
elm.addClass('has-visited');
ctrl.$hasFocus = false;
ctrl.$hasVisited = true;
if(!scope.$$phase) scope.$digest();
});
elm.closest('form').on('submit', function () {
elm.addClass('has-visited');
ctrl.$hasFocus = false;
ctrl.$hasVisited = true;
if(!scope.$$phase) scope.$digest();
})
}
}
};
app.directive('input', focusDirective);
So, you will have hasFocus property if field is focused now and hasVisited property if that field blured one or more times:
<form name="form" ng-submit="doSomething()" novalidate>
<input type="text" name="phone" ng-model="phoneNumber" phone-number/>
<p ng-show="form.phone.$invalid">[error] Wrong phone number</p>
<p ng-show="form.phone.$invalid
&& form.phone.$hasVisited">[error && visited] Wrong phone number</p>
<p ng-show="form.phone.$invalid
&& !form.phone.$hasFocus">[error && blur] Wrong phone number</p>
<div><input type="submit" value="Submit"/></div>
</form>
Demo: http://jsfiddle.net/zVpWh/4/

I fixed it with the following.
<button class="submitButton form-control" type="submit" ng-mousedown="this.form.submit();" >

Related

New line issue in Angularjs

I have a form in AngularJS. In the form I have a field named "description".
If user enters description as:
This is description:
1)point 1
2)point 2
I am saving this as :
"This is description:<br/>1)point 1 <br/> 2)point 2"
Now after saving it,to show it on page I am using something like :
<span class="summary"><em ng-bind-html="(x.DES)"></em></span>
This code is working.
If users click on the record then I am loading the form in edit mode :
the form having the line in edit mode to show the description:
<textarea ng-focus="onFocusDescrption()" maxlength="600" name="cepDes" class="form-control" rows="3" cols="16" ng-model="description" ng-disabled="isDescDisable" placeholder="Enter a description ..." id="description"></textarea>
Now the problem comes here. In controller I am setting the model value as :
$scope.description = $scope.timeEntry.DES;
Where in $scope.timeEntry.DES is having the value which is saved. The value is displayed in textarea having <br>.
It doesn't seem to be possible to use markup in a textarea as you can see in this plunkr I have created, and more information in this post. A solution would be to use a directive which allows for editable content. This way you can use a div for displaying and editing:
<div ng-bind-html="modelname"
contenteditable="true"
ng-model="modelname">
</div>
The directive (also on github):
app.directive("contenteditable", function() {
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attrs, ngModel) {
function read() {
ngModel.$setViewValue(element.html());
}
ngModel.$render = function() {
element.html(ngModel.$viewValue || "");
};
element.bind("blur keyup change", function() {
scope.$apply(read);
});
}
};
});

Detect clicked element by using directive

HTML
<div my-dir>
<tour step=" currentstep">
<span tourtip="Few more steps to go."
tourtip-next-label="Close"
tourtip-placement="bottom"
tourtip-offset="80"
tourtip-step="0">
</span>
</tour>
</div>
I have written below directive to detect the x element of tour directive.But it always shows the parent div element even though I have clicked the x.So how can I do this ? Thanks in advance.
Directive
.directive('myDir', [
'$document',
function($document) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
element.on('click', function(e) {
scope.$apply(function() {
if (element[0].className === 'tour-close-tip') {
console.log('my task');
}
});
e.stopPropagation(); //stop event from bubbling up to document object
});
}
};
}
]);
UI
This is the generated HTML on the browser:
<div hide-element-when-clicked-out-side="" class="ng-scope">
<tour step=" currentstep" class="ng-scope">
<span tourtip="Few more steps to go.!" tourtip-next-label="Close" tourtip-placement="bottom" tourtip-offset="80" tourtip-step="0" class="ng-scope">
</span><div class="tour-tip" tour-popup="" style="display: block; top: 80px; left: 0px;">
<span class="tour-arrow tt-bottom"></span>
<div class="tour-content-wrapper">
<p ng-bind="ttContent" class="ng-binding">Few more steps to go.!</p>
<a ng-click="setCurrentStep(getCurrentStep() + 1)" ng-bind="ttNextLabel" class="small button tour-next-tip ng-binding">Close</a>
<a ng-click="closeTour()" class="tour-close-tip">×</a>
</div>
</div>
Can you tell me how to access class="tour-close-tip" element within the above directive ? For me it always shows the ng-scope as the class.
You can either bind directly to that element or check which element has been clicked on, using the target attribute:
element.on('click', function (e) {
scope.$apply(function () {
if (angular.element(e.target).hasClass('tour-close-tip')) {
Your eventListener is not on the X but on the outer div element. One option would be to add the listener to the X element using a query selector on the element
You could try something like the following to get the X span and add the listener
element[0].querySelector('span').on...
Another probably better approach would be to use event delegation such as
element.on('click', selector, function(e){
});
Edit: I see your comment regarding not using JQuery so this may not work as Angular doesn't support event delegation with .on as far as I am aware.
you could use this:
app.directive('myDir', [
'$document',
function($document) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
var x = angular.element(document.querySelector('.tour-close-tip'));
x.bind('click', function() {
console.log('clicked');
});
}
};
}
]);
here's a demo plnkr:
http://plnkr.co/edit/cUCJRetsqKmSbpI0iNoJ?p=preview
there's a heading with class 'tour-close-tip' there, and we attached a click event to it.
try it out, click the heading and look in your browser's console.
from this demo hopefuly you can make progress with your code.

Angular binding does not work in data- attribute

I am using some css html template that comes with many html components and with lots of data-attributes for various things. For example for slider it has something like
<div class="slider slider-default">
<input type="text" data-slider class="slider-span" value="" data-slider-orientation="vertical" data-slider-min="0" data-slider-max="200" data-slider-value="{{ slider }}" data-slider-selection="after" data-slider-tooltip="hide">
</div>
Here I am trying to bind the value
data-slider-value="{{ slider }}"
But it's not working. Variable 'slider' is set in the $scope as:
$scope.slider = 80;
Same value 80 shows up right when I bind it as:
<h4>I have {{ slider }} cats</h4>
I have also tried
ng-attr-data-slider-value="{{ slider }}"
It didn't work.
Update
The directive has something like this
function slider() {
return {
restrict: 'A',
link: function (scope, element) {
element.slider();
}
}
};
where element.slider(); calls the code in bootstrap-slider.js (from here) for each of the sliders.
I played with this for a while, and came up with a few options for you. See my Plunkr to see them in action.
Option 1: No need to update the scope value when the slider changes
This will work with the HTML from your question. The following is what you should change the directive code to.
app.directive('slider', function slider() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
attrs.$observe('sliderValue', function(newVal, oldVal) {
element.slider('setValue', newVal);
});
}
}
});
Option 2: Two way binding to the scope property
If you need the scope property to be updated when the slider handle is dragged, you should change the directive to the following instead:
app.directive('sliderBind', ['$parse',
function slider($parse) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var val = $parse(attrs.sliderBind);
scope.$watch(val, function(newVal, oldVal) {
element.slider('setValue', newVal);
});
// when the slider is changed, update the scope
// property.
// Note that this will only update it when you stop dragging.
// If you need it to happen whilst the user is dragging the
// handle, change it to "slide" instead of "slideStop"
// (this is not as efficient so I left it up to you)
element.on('slideStop', function(event) {
// if expression is assignable
if (val.assign) {
val.assign(scope, event.value);
scope.$digest();
}
});
}
}
}
]);
The markup for this changes slightly to:
<div class="slider slider-default">
<input type="text" data-slider-bind="slider2" class="slider-span" value="" data-slider-orientation="vertical" data-slider-min="0" data-slider-max="200" data-slider-selection="after" data-slider-tooltip="hide" />
</div>
Note the use of the data-slider-bind attribute to specify the scope property to bind to, and the lack of a data-slider-value attribute.
Hopefully one of these two options is what you were after.
I use .attr and it works for me. Try this:
attr.data-slider-value="{{slider}}"

AngularJS prevent ngModel sync

I have a simple directive called po-datepicker, it displays a datepicker on the screen, but allows the user to type a date manually:
<input type="text" ng-model="model" po-datepicker required />
and this is the directive:
myApp.directive('poDatepicker', function () {
return {
require: ['?^ngModel'],
restrict: 'A',
link: function ($scope, elem, attrs, ctrl) {
var ngModel = ctrl[0];
var picker = elem.datepicker();
picker.on('changeDate', function(e) {
ngModel.$setViewValue(e.date);
...
});
elem.parent().find('button').on('click', function() {
picker.datepicker('show');
});
var changeFn = function(e) {
// Here I have some logic that calls $setViewValue();
};
picker.on('hide', changeFn);
elem.on('keyup blur', changeFn);
}
};
});
this works as expected, but when I try to type a value in the input, it updates the ngModel, changing the variable in the scope, how can I prevent ngModel from being changed in the input?
Here is a plunkr, try manually writing a value and you'll understand what I'm talking.
Actually, after some research, I found a solution for this problem.
What I found on forums and questions is that I needed to unbind the element's events, like this:
elem.unbind('input').unbind('keydown').unbind('change');
But that solution didn't work as expected.
The problem is that I'm currently using Angular 1.2.x, I found out that you need also to set some priority to the directive, such as:
return {
require: ['?^ngModel'],
priority: 1,
...
}
The priority: 1 is needed in this case, because of the priority of some internal Angular.js directives.
Here is an updated plunker with the right priority set up.
Just add 'disabled' to the input http://plnkr.co/edit/xFeAmSCtKdNSQR1zbAsd?p=preview
<input type="text" class="form-control" ng-model="test" po-datepicker required feedback disabled/>

How do I use a directive to toggle a slide animation on an element from my controller?

I am confused on the following scenario. Let's say I have a table with rows. When a user clicks a button in the table I want a user form to slide down with jQuery and display the form with the selected row values. Here is what I am currently doing that doesn't quite make sense:
View
<tr ng-click="setItemToEdit(item)" slide-down-form>
...
<form>
<input type="test" ng-model={{itemToEdit.Property1}} >
<button ng-click=saveEditedItem(item)" slide-up-form>
<form>
Control
$scope.itemToEdit = {};
$scope.setItemToEdit = function(item) {
$scope.itemToEdit = item;
});
$scope.saveEditedItem = function(item) {
myService.add(item);
$scope.itemToEdit = {};
}
Directive - Slide-Up / Slide-Down
var linker = function(scope, element, attrs) {
$(form).slideUp(); //or slide down
}
It seems the my directive and my control logic are too disconnected. For example, what happens if there is a save error? The form is already hidden because the slideUp event is complete. I'd most likely want to prevent the slideUp operation in that case.
I've only used AngularJS for about a week so I'm sure there is something I'm missing.
Sure, it's a common problem... here's one way to solve this: Basically use a boolean with a $watch in a directive to trigger the toggling of your form. Outside of that you'd just set a variable on your form to the object you want to edit.
Here's the general idea in some psuedo-code:
//create a directive to toggle an element with a slide effect.
app.directive('showSlide', function() {
return {
//restrict it's use to attribute only.
restrict: 'A',
//set up the directive.
link: function(scope, elem, attr) {
//get the field to watch from the directive attribute.
var watchField = attr.showSlide;
//set up the watch to toggle the element.
scope.$watch(attr.showSlide, function(v) {
if(v && !elem.is(':visible')) {
elem.slideDown();
}else {
elem.slideUp();
}
});
}
}
});
app.controller('MainCtrl', function($scope) {
$scope.showForm = false;
$scope.itemToEdit = null;
$scope.editItem = function(item) {
$scope.itemToEdit = item;
$scope.showForm = true;
};
});
markup
<form show-slide="showForm" name="myForm" ng-submit="saveItem()">
<input type="text" ng-model="itemToEdit.name" />
<input type="submit"/>
</form>
<ul>
<li ng-repeat="item in items">
{{item.name}}
<a ng-click="editItem(item)">edit</a>
</li>
</ul>

Resources