Compiling Element Causes Input Caret Position to Move to End - angularjs

I am having a problem with a directive. The purpose of the directive is to easily add validation without having to manually add ng-class (among other things) to elements in order to get the error state to show up. I just simply want to put a "validation" directive on my element and have the appropriate classes (and error messages) be generated when there is an error state.
As far as the validation goes, it is working great, but it causes an odd side effect. Whenever I am editing a value in an input box that has the validation directive on it, it moves the caret to the end of the text in the input field. It appears to be the fact that I'm compiling the element (in this case the parent element which contains this element).
Here is a jsbin showing the problem. To reproduce, type a value in the field, then put the caret in the middle of the value you just typed and try typing another character. Notice it moves you to the end of the field. Notice that if you delete the value, the field label turns red as expected to show a validation error (the field is required).
Here is the directive (from the jsbin):
angular.module('app', [])
.directive('validation', function($compile) {
return {
require: 'ngModel',
restrict: 'A',
compile: function(compileElement, attrs) {
var formName = compileElement[0].form.name;
compileElement.removeAttr('validation');
compileElement.parent().attr('ng-class', formName + "['" + attrs.name + "'].$invalid && " + formName + "['" + attrs.name + "'].$dirty ? 'error' : ''");
return function(scope, element) {
$compile(element.parent())(scope);
}
}
};
});
And here is the html:
<html>
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.1/angular.min.js"></script>
</head>
<body ng-app="app">
<form name="subscribeForm">
<label>
First Name
<input type="text"
id="firstName"
name="firstName"
ng-model="userInfo.FirstName"
required
validation/>
</label>
</form>
</body>
</html>

not sure if you've figured this out but I encountered a similar problem. found a solution at Preserving cursor position with angularjs. for convenience, below is the directive snippet that would solve this issue.
app.directive('cleanInput', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelController) {
var el = element[0];
function clean(x) {
return x && x.toUpperCase().replace(/[^A-Z\d]/g, '');
}
ngModelController.$parsers.push(function(val) {
var cleaned = clean(val);
// Avoid infinite loop of $setViewValue <-> $parsers
if (cleaned === val) return val;
var start = el.selectionStart;
var end = el.selectionEnd + cleaned.length - val.length;
// element.val(cleaned) does not behave with
// repeated invalid elements
ngModelController.$setViewValue(cleaned);
ngModelController.$render();
el.setSelectionRange(start, end);
return cleaned;
});
}
}
});
the directive had a different purpose though so modify it according to your requirements.

If you're not using the built in validation model/process you're doing it wrong. Check out the tutorial on the angular-js website:
http://code.angularjs.org/1.2.13/docs/guide/forms
Also, you shouldn't be doing element manipulation in the compile stage.
Update
You need to have a look at the section called Custom Validation.
Use the ctrl.$parsers approach. You add your parser on to the list of parsers and your fn will run any time the model changes. You then use the ctrl.$setValidity('strNameOfValidation', true) to set the validity. Angular will then add a class for you - called .ng-valid-float or .ng-invalid-float.
var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
app.directive('smartFloat', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if (FLOAT_REGEXP.test(viewValue)) {
ctrl.$setValidity('float', true);
return parseFloat(viewValue.replace(',', '.'));
} else {
ctrl.$setValidity('float', false);
return undefined;
}
});
}
};
});

Related

angularjs textarea with colors (with html5 contenteditable)

I'm trying to create an editor which does "syntax highlighting",
it is rather simple:
yellow -> <span style="color:yellow">yellow</span>
I'm also using <code contenteditable> html5 tag to replace <textarea>, and have color output.
I started from angularjs documentation, and created the following simple directive. It does work, except it do not update the contenteditable area with the generated html.
If I use a element.html(htmlTrusted) instead of ngModel.$setViewValue(htmlTrusted), everything works, except the cursor jumps to the beginning at each keypress.
directive:
app.directive("contenteditable", function($sce) {
return {
restrict: "A", // only activate on element attribute
require: "?ngModel", // get ng-model, if not provided in html, then null
link: function(scope, element, attrs, ngModel) {
if (!ngModel) {return;} // do nothing if no ng-model
element.on('blur keyup change', function() {
console.log('app.directive->contenteditable->link->element.on()');
//runs at each event inside <div contenteditable>
scope.$evalAsync(read);
});
function read() {
console.log('app.directive->contenteditable->link->read()');
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
html = html.replace(/</, '<');
html = html.replace(/>/, '>');
html = html.replace(/<span\ style=\"color:\w+\">(.*?)<\/span>/g, "$1");
html = html.replace('yellow', '<span style="color:yellow">yellow</span>');
html = html.replace('green', '<span style="color:green">green</span>');
html = html.replace('purple', '<span style="color:purple">purple</span>');
html = html.replace('blue', '<span style="color:yellow">blue</span>');
console.log('read()-> html:', html);
var htmlTrusted = $sce.trustAsHtml(html);
ngModel.$setViewValue(htmlTrusted);
}
read(); // INITIALIZATION, run read() when initializing
}
};
});
html:
<body ng-app="MyApp">
<code contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>This <span style="color:purple">text is purple.</span> Change me!</code>
<hr>
<pre>{{userContent}}</pre>
</body>
plunkr: demo (type yellow, green or blue into the change me input area)
I tried scope.$apply(), ngModel.$render() but has no effect. I must miss something really obvious...
The links I already read through:
others' plunker demo 1
others' plunker demo 2
angularjs documentation's example
$sce.trustAsHtml stackoverflow question
setViewValue stackoverflow question
setViewValue not updating stackoverflow question
Any help is much appreciated. Please see the plunker demo above.
After almost a year, I finally settled to Codemirror, and I was never happier.
I'm doing side-by-side markdown source editing with live update (with syntax highlighting, so even a bit more advanced than stackoverflow's editing page.)
I created a simple codeEditor angular directive, which requires codeMirror, and uses it.
For completeness, here is the component sourcecode:
$ cat components/codeEditor/code-editor.html
<div class="code-editor"></div>
$ cat codeEditor.js
'use strict';
angular.module('myApp')
.directive('codeEditor', function($timeout, TextUtils){
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
scope: {
syntax: '#',
theme: '#'
},
templateUrl: 'components/codeEditor/code-editor.html',
link: function(scope, element, attrs, ngModelCtrl, transclude){
// Initialize Codemirror
var option = {
mode: scope.syntax || 'xml',
theme: scope.theme || 'default',
lineNumbers: true
};
if (option.mode === 'xml') {
option.htmlMode = true;
}
scope.$on('toedit', function () { //event
//This is required to correctly refresh the codemirror view.
// otherwise the view stuck with 'Both <code...empty.' initial text.
$timeout(function() {
editor.refresh();
});
});
// Require CodeMirror
if (angular.isUndefined(window.CodeMirror)) {
throw new Error('codeEditor.js needs CodeMirror to work... (o rly?)');
}
var editor = window.CodeMirror(element[0], option);
// Handle setting the editor when the model changes if ngModel exists
if(ngModelCtrl) {
// Timeout is required here to give ngModel a chance to setup. This prevents
// a value of undefined getting passed as the view is rendered for the first
// time, which causes CodeMirror to throw an error.
$timeout(function(){
ngModelCtrl.$render = function() {
if (!!ngModelCtrl.$viewValue) {
// overwrite <code-editor>SOMETHING</code-editor>
// if the $scope.content.code (ngModelCtrl.$viewValue) is not empty.
editor.setValue(ngModelCtrl.$viewValue); //THIRD happening
}
};
ngModelCtrl.$render();
});
}
transclude(scope, function(clonedEl){
var initialText = clonedEl.text();
if (!!initialText) {
initialText = TextUtils.normalizeWhitespace(initialText);
} else {
initialText = 'Both <code-editor> tag and $scope.content.code is empty.';
}
editor.setValue(initialText); // FIRST happening
// Handle setting the model if ngModel exists
if(ngModelCtrl){
// Wrap these initial setting calls in a $timeout to give angular a chance
// to setup the view and set any initial model values that may be set in the view
$timeout(function(){
// Populate the initial ng-model if it exists and is empty.
// Prioritize the value in ngModel.
if(initialText && !ngModelCtrl.$viewValue){
ngModelCtrl.$setViewValue(initialText); //SECOND happening
}
// Whenever the editor emits any change events, update the value
// of the model.
editor.on('change', function(){
ngModelCtrl.$setViewValue(editor.getValue());
});
});
}
});
// Clean up the CodeMirror change event whenever the directive is destroyed
scope.$on('$destroy', function(){
editor.off('change');
});
}
};
});
There is also inside the components/codeEditor/vendor directory the full codemirror sourcecode.
I can highly recommend codeMirror. It is a rocksolid component, works in
every browser combination (firefox, firefox for android, chromium).

Toggle edit and display of the fields in a form

Above all, I have the plnkr at here.
I am trying to create a series of directive that support in-place toggle of text display and edit within a form. As I understand, there is a similar module like xeditable available, but we need to do something different down the road. So I started with an experiment to start with something similar.
First, I create a directive that allows toggling edit/display by setting an attribute editEnabled on the directive called editableForm. The following code does not do anything special other than a line of log message.
function editableForm ($log) {
var directive = {
link: link,
require: ['form'],
restrict: 'A',
scope: {
editEnabled: "&editEnabled"
}
};
return directive;
function link(scope, element, attrs, controller) {
//$log.info('editEnabled: ' + scope.editEnabled());
$log.info('editEnabled: ' + attrs.editEnabled); //this also works
}
} //editableForm
Then I wrote the following directive to override the input tag in html:
//input directive
function input($log) {
var directive = {
link: link,
priority: -1000,
require: ['^?editableForm', '?ngModel'],
restrict: 'E'
};
return directive;
function link(scope, element, attrs, ngModel) {
ngModel.$render = function() {
if (!ngModel.$viewValue || !ngModel.$viewValue) {
return;
}
element.text(ngModel.$viewValue);
};
$log.info('hello from input');
$log.info('input ngModel: ' + attrs.ngModel);
// element.val('Hello');
scope.$apply(function() {
ngModel.$setViewValue('hello');
ngModel.$render();
});
}
} //input
I was trying to show the ngModel value of the input as text in the input directive, however, it doesn't seem to do anything in my testing. Could someone spot where I am doing wrong? I wish to replace each input fields with text/html (e.g. <span>JohnDoe</span> for Username).
My first attempt on input is a proof of concept. If it works, I will keep working on other tags like button, select, etc.
Long shot here... Your requiring both editableForm and ngModel in your input directive. So the fourth parameter of your link function should be an array of controllers in the respective order of the require array, not the ngModel controller as you are expecting.
I didnt go any further in examining your code, but check it out.

Angular validation of dependent text field in an ngRepeat tag

I have the following ngRepeat code:
<div ng-repeat="drug in drugs | orderBy:'drugName'">
<input id="{{drug.drugName}}"
class="drugCheckbox"
name="{{drug.drugName}}"
type="checkbox" value="{{drug.drugName}}"
ng-model="foobar"
validate-foo
/>
{{drug.drugName}}
<!-- this is the error message, one per each repeat element -->
<span style="color:red" ng-show="myForm.{{drug.drugName}}.$error.summary">
Fill in the summary
</span>
<input type="text"
name="summary-{{drug.drugName}}"
id="summary-{{drug.drugName}}"
placeholder="Summary"/>
</div>
My validateFoo directive:
app.directive('validateFoo', function(){
return{
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
var id= attr.id;
//if the checkbox is checked, see if the text field has been filled in
if(viewValue) {
var val= document.getElementById('summary-' + id).value;
if(val.length == 0) {
ctrl.$setValidity("summary", false);
return undefined;
}
else {
ctrl.$setValidity(id, true);
return viewValue;
}
}
});
}
}
});
I cannot get the validation to work for any of the checkboxes, I am not clear on what to use for "ng-show" in the error message span element.
First of all that's because all fields will be registered in your formController under name {{drug.drugName}}.
Second thing is that there is no myForm.{{drug.drugName}}.$invalid in scope. You probably tried to do myForm[drug.drugName].$invalid. But it will not work anyway - look at 1.
I think that there still is no proper way to set name field dynamically in ng-repeat. Instead you need to create tour own directive, that will build repeated list elements as String, then append it to your HTML and $compile it place.
EDIT
I have developed one solution:
http://jsfiddle.net/ulfryk/gejfeLp7/
EDIT 2:
I've developed even simpler solution ( http://jsfiddle.net/ulfryk/5wq7539r/ ):
app.directive('fixRepeatedModelName', function () {
return {
link: function (scope, element, attrs, ngModelCtrl) {
if (!attrs.name) return;
ngModelCtrl.$name = attrs.name;
},
priority: '-100',
require: 'ngModel'
};
});
And add fix-repeated-model-name attribute to any ng-model with dynamic ({{ }} dependent) name placed inside ng-repeat. And it now works :)

AngularJS: writing a capatilize directive does not seem to work

I am new to AngularJS so if i'm completely off track i apologize.
Template (which is iterated):
<div class="title">
<input ng-model="menuItem.kind" capitalize-first />
</div>
Directive (to capitalize):
angular.module('phonecat', []).directive('capitalizeFirst', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var capitalize = function(inputValue) {
var capitalized = inputValue.charAt(0).toUpperCase() +
inputValue.substring(1);
if(capitalized !== inputValue) {
modelCtrl.$setViewValue(capitalized);
modelCtrl.$render();
}
return capitalized;
}
modelCtrl.$parsers.push(capitalize);
capitalize(scope[attrs.ngModel]); // capitalize initial value
}
};
});
The result is a blank span. Any ideas?
You can't use scope[attrs.ngModel]. AngularJs will not set scope['menuItem.kind'] = ..., but scope.menuItem.kind = ....
Additionally, the value might had been resolved to a parent scope. So your best choice is to use ngModel.$modelValue to get the current model value of ngModel (capitalize(modelCtrl.$modelValue);).
This leads to another problem. You don't have this value in linking phase, it's lightly async. To workaround it, all you need is to inject $timeout and run this code asynchronously:
$timeout(function() {
capitalize(modelCtrl.$modelValue);
}, 0);
So, summarizing, all you need is to change your directive last line to the one above, and this will solve the problem. All the rest is ok, take a look at this Plnkr.

How to disable angulars type=email validation?

How would you go about disabling, or at the very least changing, how Angular validates type=email inputs?
Currently, if you use type=email, Angular essentially double validates.. as the Browser (Chrome in this case) validates the email, and then angular does too. Not only that, but what is valid in Chrome foo#bar is not valid in Angularjs.
The best i could find, is ng-pattern, but ng-pattern simply adds a 3rd pattern validation for the input type.. instead of replacing Angular's email validation. heh
Any ideas?
Note: This is example is for angular 1.2.0-rc.3. Things might behave differently on other versions
Like others have stated it is a bit complex to turn off angulars default input validation. You need to add your own directive to the input element and handle things in there. Sergey's answer is correct, however it presents some problems if you need several validators on the element and don't want the built in validator to fire.
Here is an example validating an email field with a required validator added. I have added comments to the code to explain what is going on.
Input element
<input type="email" required>
Directive
angular.module('myValidations', [])
.directive('input', function () {
var self = {
// we use ?ngModel since not all input elements
// specify a model, e.g. type="submit"
require: '?ngModel'
// we need to set the priority higher than the base 0, otherwise the
// built in directive will still be applied
, priority: 1
// restrict this directive to elements
, restrict: 'E'
, link: function (scope, element, attrs, controller) {
// as stated above, a controller may not be present
if (controller) {
// in this case we only want to override the email validation
if (attrs.type === 'email') {
// clear this elements $parsers and $formatters
// NOTE: this will disable *ALL* previously set parsers
// and validators for this element. Beware!
controller.$parsers = [];
controller.$formatters = [];
// this function handles the actual validation
// see angular docs on how to write custom validators
// http://docs.angularjs.org/guide/forms
//
// in this example we are not going to actually validate an email
// properly since the regex can be damn long, so apply your own rules
var validateEmail = function (value) {
console.log("Validating as email", value);
if (controller.$isEmpty(value) || /#/.test(value)) {
controller.$setValidity('email', true);
return value;
} else {
controller.$setValidity('email', false);
return undefined;
}
};
// add the validator to the $parsers and $formatters
controller.$parsers.push(validateEmail);
controller.$formatters.push(validateEmail);
}
}
}
};
return self;
})
// define our required directive. It is a pretty standard
// validation directive with the exception of it's priority.
// a similar approach must be take with all validation directives
// you would want to use alongside our `input` directive
.directive('required', function () {
var self = {
// required should always be applied to a model element
require: 'ngModel'
, restrict: 'A'
// The priority needs to be higher than the `input` directive
// above, or it will be removed when that directive is run
, priority: 2
, link: function (scope, element, attrs, controller) {
var validateRequired = function (value) {
if (value) {
// it is valid
controller.$setValidity('required', true);
return value;
} else {
// it is invalid, return undefined (no model update)
controller.$setValidity('required', false);
return undefined;
}
};
controller.$parsers.push(validateRequired);
}
};
return self;
})
;
There you have it. You now have control over type="email" input validations. Please use a proper regex to test the email though.
One thing to note is that in this example validateEmail is run before validateRequired. If you need validateRequired to run before any other validations, then just prepend it to the $parsers array (using unshift instead of push).
Very simple. I had to alter the email regex to match a business requirement, so I made this directive that makes the email regex customizable. It essentially overwrites the original validator with my custom one. You don't have to mess with all the $parsers and $formatters (unless I'm missing something). So my directive was this...
module.directive('emailPattern', function(){
return {
require : 'ngModel',
link : function(scope, element, attrs, ngModel) {
var EMAIL_REGEX = new RegExp(attrs.emailPattern, "i");
ngModel.$validators["email"] = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return ngModel.$isEmpty(value) || EMAIL_REGEX.test(value);
};
}
}
});
Then use it like this, supplying whatever email pattern you personally want:
<input type="email" email-pattern=".+#.+\..+"/>
But if you just want to permanently disable it then you could do this.
module.directive('removeNgEmailValidation', function(){
return {
require : 'ngModel',
link : function(scope, element, attrs, ngModel) {
ngModel.$validators["email"] = function () {
return true;
};
}
}
});
Then use it like this...
<input type="email" remove-ng-email-validation>
On HTML5 you can use the form's attribute novalidate to disable browser's validation:
<form novalidate>
<input type="email"/>
</form>
If you want to create a custom validator in angularjs, you have a good tutorial and example here: http://www.benlesh.com/2012/12/angular-js-custom-validation-via.html
Echoing nfiniteloop, you don't need to mess with the $parsers or $formatters to override the default validators. As referenced in the Angular 1.3 docs, the $validators object is accessible on the ngModelController. With custom directives you can write as many different email validation functions as you need and call them wherever you want.
Here's one with a very nice standard email format regex from tuts: 8 Regular Expressions You Should Now (probably identical to Angular's default, idk).
var app = angular.module('myApp', []);
app.directive('customEmailValidate', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
var EMAIL_REGEXP = /^([a-z0-9_\.-]+)#([\da-z\.-]+)\.([a-z\.]{2,6})$/;
ctrl.$validators.email = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
if (EMAIL_REGEXP.test(viewValue)) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
};
});
Here's one that removes validation entirely:
var app = angular.module('myApp', []);
app.directive('noValidation', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.email = function(modelValue, viewValue) {
// everything is valid
return true;
};
}
};
});
To use in your markup:
<!-- 'test#example.com' is valid, '#efe#eh.c' is invalid -->
<input type="email" custom-email-validate>
<!-- both 'test#example.com' and '#efe#eh.c' are valid -->
<input type="email" no-validation>
In my project I do something like this (custom directive the erases all other validations including ones installed by angularjs):
angular.module('my-project').directive('validEmail', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl){
var validator = function(value){
if (value == '' || typeof value == 'undefined') {
ctrl.$setValidity('validEmail', true);
} else {
ctrl.$setValidity('validEmail', /your-regexp-here/.test(value));
}
return value;
};
// replace all other validators!
ctrl.$parsers = [validator];
ctrl.$formatters = [validator];
}
}
});
How to use it (note novalidate, it's required to turn off browser validation):
<form novalidate>
<input type="email" model="email" class="form-control" valid-email>

Resources