Angularjs generic input directive with integrated validation - angularjs

I have been trying to write a generic input directive with integrated validation. But I cant seem to get the validation to fire (ng-show).
My guess is I don't really have a grasp on when the items are being evaluated by angular and added to the watch list but I am not sure.
My actual control is more complicated but have created a simplified version to illustrate the issue.
The generated HTML (viewed with inspector) looks like it should work.
What am I missing? Here is the: Plunker Validation Test
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script data-require="angular.js#1.4.7" data-semver="1.4.7" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<form name='myForm'>
<h1>Validation Test</h1>
Name: <my-input uname='userName'></my-input><br>
Phone: <my-input uname='phone'></my-input><br>
</form>
<script>
var app = angular.module('myApp',[]);
app.directive('myInput',function(){
return {
requires: '^form',
replace:true,
scope:{
uname:'='
},
restrict: 'E',
template: function(element,attr){
return '<div><input class="reqclass" ng-model="' + attr.uname + '" name="' + attr.uname + '" ng-minlength="5"></input><span ng-show="myForm.' + attr.uname + '.$error.minlength">Too short!</span></div>'
}
};
});
</script>
</body>
</html>

For anyone else with similar issue.
Thanks for comment, it led me to answer. See revised code below.
Issue was access to form in isolated scope as well as attempting to use form name rather than scope.form object in span.
NOTE the require:['^form','?ngModel'] and in link: scope.form = ctrls[0] and revised ng-show="form.' + attr.uname + '.$error.minlength"
Plunker above has also been revised to reflect correction
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script data-require="angular.js#1.4.7" data-semver="1.4.7" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<form name='form'>
<h1>Validation Test</h1>
Name: <my-input uname='userName'></my-input><br>
Not Directive: <div><input class="reqclass" name="city" ng-model="city" ng-minlength="5"/><span ng-show="form.city.$error.minlength">Too short!</span></div><br>
Phone: <my-input uname='phone'></my-input><br>
</form>
<script>
var app = angular.module('myApp',[]);
app.directive('myInput',function(){
return {
require: ['^form', '?ngModel'],
replace:false,
scope:{
uname:'='
},
restrict: 'E',
template: function(element,attr){
return '<div><input class="reqclass" name="' + attr.uname + '" ng-model="' + attr.uname + '" ng-minlength="5"/><span ng-show="form.' + attr.uname + '.$error.minlength">Too short!</span></div>'
},
link: function(scope, element, attrs, ctrls) {
scope.form = ctrls[0];
var ngModel = ctrls[1];
}
}
});
</script>
</body>
</html>

Related

Wrapping md-tabs in a directive gives an "Orphan ngTransclude Directive" error

I'd like to create a directive that wraps md-tabs, but I'm getting an error, "Orphan ngTransclude Directive". I've replicated the error in this snippet:
angular.module('transcludeExample', ['ngMaterial'])
.directive('worksGreat', function(){
return {
restrict: 'E',
transclude: true,
template: '<ng-transclude></ng-transclude>'
};
})
.directive('doesntWork', function(){
return {
restrict: 'E',
transclude: true,
template: '' +
'<md-tabs md-dynamic-height>' +
'<md-tab label=\'tab 1\'>' +
'<ng-transclude></ng-transclude>' +
'</md-tab>' +
'</md-tabs>'
};
})
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example - example-simpleTranscludeExample-production</title>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/0.11.2/angular-material.min.css">
</head>
<body ng-app="transcludeExample">
<!-- Angular Material Dependencies -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-animate.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-aria.js"></script>
<!-- Angular Material Javascript using GitCDN to load directly from `bower-material/master` -->
<script src="https://gitcdn.link/repo/angular/bower-material/master/angular-material.js"></script>
<div>
<h3>ng-transclude in a directive works great:</h3>
<works-great>Inner text</works-great>
<hr/>
<h3>md-tabs without a directive works great:</h3>
<md-tabs md-dynamic-height>
<md-tab label="tab 1">
Inner text
</md-tab>
</md-tabs>
<hr/>
<h3>combining md-tabs with a directive doesn't work:</h3>
<doesnt-work>Inner text</doesnt-work>
</div>
</body>
</html>
I found this answer that gets into manually manipulating elements outside of the template, but I'm hoping for a cleaner "more angular" way. What's going on here? Is there a way I can define what directive the ng-transclude should apply to?
This comment on github presents the solution. ng-transclude is designed to be generic so that it can work with any directive, but in this case, that's where the problem comes from. Fortunately, it's extremely simple to mimic, and we can specify what parent it should apply to with require.
I've updated my code snippet with a working solution:
var orphanDemoCtrl = function($transclude){
this.$transclude = $transclude;
}
angular.module('transcludeExample', ['ngMaterial'])
.controller('orphanDemoCtrl', orphanDemoCtrl)
.directive('orphanDemo', function(){
return {
restrict: 'E',
transclude: true,
template: '' +
'<md-tabs md-dynamic-height>' +
'<md-tab label=\'tab 1\'>' +
'<orphan-demo-transclude></orphan-demo-transclude>' +
'</md-tab>' +
'</md-tabs>',
controller: orphanDemoCtrl
};
})
.directive('orphanDemoTransclude', function(){
return {
require: "^orphanDemo",
link: function($scope, $element, $attrs, orphanDemoCtrl){
orphanDemoCtrl.$transclude(function(clone) {
$element.empty();
$element.append(clone);
});
}
}
})
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example - example-simpleTranscludeExample-production</title>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/0.11.2/angular-material.min.css">
</head>
<body ng-app="transcludeExample">
<!-- Angular Material Dependencies -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-animate.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-aria.js"></script>
<!-- Angular Material Javascript using GitCDN to load directly from `bower-material/master` -->
<script src="https://gitcdn.link/repo/angular/bower-material/master/angular-material.js"></script>
<div>
<h3>combining md-tabs with a directive works now:</h3>
<orphan-demo>Inner text</orphan-demo>
</div>
</body>
</html>

Watch a custom directives inner html in angular

I have a global search variable that is used by the whole app
newspaper.controller("MainController", function($scope) {
$scope.search = {query:''};
});
Then I have a contenteditable div that I want to bind to $scope.search
app.directive('search', function() {
return {
restrict: 'AE',
replace: true,
template: '<div ng-model="query" id="search"></div>',
scope: {
query: '='
},
controller: function($scope) {
// I want to watch the value of the element
$scope.$watch('query', function(newValue, oldValue){
console.log(newValue);
},true);
},
link: function(scope, element, attrs) {
// Medium JS content editable framework
new Medium({
element: element[0],
mode: Medium.inlineMode
});
}
}
});
The watch is not firing when I type new values into the div, I guess Im still confused on how to link a directive with a model. Here's the HTML
<nav ng-controller="MainControllerr">
<search context="search.query"></np-search>
</nav>
Don't think you need the watch. It's bad practise to use watch functions in your controllers anyway as it makes them really hard to test.
Here's a simplified version of what (I think) your trying to do.
DEMO
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.4.x" src="https://code.angularjs.org/1.4.2/angular.js" data-semver="1.4.2"></script>
<script src="app.js"></script>
</head>
<body>
<nav ng-controller="MainController">
<pre>{{query}}</pre>
<search query="query"></search>
</nav>
</body>
</html>
app.js
var app = angular.module('plunker', []);
app.controller("MainController", function($scope) {
$scope.query = {value:''};
});
app.directive('search', function() {
return {
restrict: 'AE',
template: '<input ng-model="query.value" id="search"/>',
scope: {
query: '='
}
}
});
EDIT
If you really want to use a content editable div you'd have to try something like this:
Also, see this SO question from which I've taken and adapted code to create the demo below. Hope it helps.
DEMO2
index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.4.x" src="https://code.angularjs.org/1.4.2/angular.js" data-semver="1.4.2"></script>
<script src="app.js"></script>
</head>
<body>
<nav ng-controller="MainController">
<pre>{{query}}</pre>
<div search contenteditable="true" ng-model="query.value">{{ query.value }}</div>
</nav>
</body>
</html>
app.js
var app = angular.module('plunker', []);
app.controller("MainController", function($scope) {
$scope.query = {value:'rrr'};
});
app.directive('search', function() {
return {
restrict: 'AE',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
element.bind('blur', function() {
scope.$apply(function() {
ctrl.$setViewValue(element.html());
});
});
element.html(ctrl.$viewValue);
}
}
});

How can I move this block of text using a custom Angular directive?

I am trying to move a block of text up or down when arrows are pressed. I am having troubles getting my directive to change its CSS values when the buttons are pressed.
Here is my HTML:
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.2.x" src="https://code.angularjs.org/1.2.22/angular.js" data-semver="1.2.22"></script>
<script src="app.js"></script>
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<script src="subtitle.js"></script>
</head>
<body ng-controller="MainCtrl">
<h1>Move Text up or down</h1>
<subtitle class="sub" pos="position">This will move when buttons are pressed</subtitle>
<br>
Position={{position}}
<br>
<br>
<p style="color:red">Click buttons to move text up or down</p>
<i class="fa fa-chevron-up" ng-click="inc()"></i>
<i class="fa fa-chevron-down" ng-click="dec()"></i>
</body>
</html>
Here is my directive:
angular.module('mSystem')
.directive('subtitle', function () {
"use strict";
var
scope = {
pos: "="
},
restrict = 'AE',
link = function($scope, $element, $attrs){
$scope.$watch('pos', function () {
console.log("position is: "+$scope.pos);
$element.css('{top:'+$scope.pos+'px;position:relative;!important}');
$attrs.style="top:'+$scope.pos+'px;position:relative;!important'";
});
};
return {
scope: scope,
link: link,
restrict: restrict
};
});
and here is my app:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.position=20;
$scope.inc=function(){
$scope.position+=1;
}
$scope.dec=function(){
console.log('subtracted');
$scope.position-=1;
}
});
I have created an angular.js plunker here: http://plnkr.co/o99x2Z
The syntax for changing the CSS is a bit different. Try this:
$element.css('top', $scope.pos + 'px');
Updated plunker
You are passing a string to the $element.css(), use an Object instead:
$element.css({ top: $scope.pos+'px', position: 'relative;!important' });
Plunker

How can I set the value of an id in the template part of a directive?

I am trying this:
http://plnkr.co/edit/IzhScWwcy6owjsPKm2Fs?p=preview
<!doctype html>
<html ng-app="plunker" >
<head>
<meta charset="utf-8">
<title>AngularJS Plunker</title>
<link rel="stylesheet" href="style.css">
<script>document.write("<base href=\"" + document.location + "\" />");</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
xx<div admin-select
admin-id="examType"></div>xx
</body>
</html>
and
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
});
app.directive("adminSelect", function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
adminId: '=adminId'
},
template: '<div id="{{adminId}}"></div>'
};
});
This is not working and I cannot see why. Can someone give me some advice and help me to set the id of the <div> that's part of the template.
You have to put the value of the admin-id attribute under apostrophe because you want to set the value. Example
<div admin-select admin-id="'examType'"></div>
Otherwise you set the scope variable in the controller and pass this variable to the directive:
app.controller('MainCtrl', function($scope) {
$scope.myId = 'examType'
});
<div admin-select admin-id="myId"></div>
Example
template: '<div id="{{$parent.adminId}}"></div>'
Angular's docs has this "the root of the template always gets a new scope.", maybe that's why.
Upddated
this answer is not working... =)

AngularJS - Create a directive that adds a sibling element

I'm creating a my-validate directive that looks something like this
<input my-validate="customValidation" ng-model="model" />
What I want to do is to attach a sybling element to the directive like this
Error template:
<ul class"errors">
<li ng-repeat="for error in errors">{{error}} not valid</li>
</ul>
errors is defined in the scope of the directive.
I've added the error template in the compile function, but the problem I have is that the scope in the link function is not the same as the attached template.
Here is a plunker to illustrate the issue: http://plnkr.co/edit/ghdtdYruQaaO0Yxxlrt1?p=preview
'world' is seen in the directive template, but not on the added element :S.
That's because your div "2 hello" is outside the container where your scope is visible.
you can use element.append() instead of element.after() to have the scope available.
Directive
var app = angular.module('plunker', []);
app.directive('myValidate', function($compile) {
return {
template: '<span>1. Hello {{world}} my scope is {{$id}} (parent: {{$parent.$id}})<span/>',
replace: true,
restrict: 'A',
scope: true,
compile: function (element) {
element.append('<div>2. Hello {{ world }}, my scope is {{$id}} (parent: {{$parent.$id}})</div>');
return function(scope) {
scope.world = 'World';
//$compile()(scope);
};
}
};
});
HTML
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script data-require="angular.js#1.1.5" data-semver="1.1.5" src="http://code.angularjs.org/1.1.5/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="app.js"></script>
</head>
<body>
<input my-validate="" />
</body>
</html>
http://plnkr.co/edit/dU3holBCePKe0ZAwQKh1?p=preview
I was reading and checking the example because I was in the same situation to display validation messages but under the input field and the message can change according to what kind of validation is required.
So I came up with this solution
var app = angular.module('app', []);
app.controller('ctrl', function($scope, CONSTANTS) {
$scope.title = "title";
$scope.CONSTANTS = CONSTANTS;
});
app.constant('CONSTANTS', {
LENGHT_1: 3,
LENGHT_2: 4
});
app.directive('dir', function($compile) {
return {
scope: true,
restrict: 'A',
require: '?ngModel',
link: function(scope, elem, attrs, ngModel) {
scope.maxLength = false;
scope.required = false;
scope.max = scope.$eval(attrs['ngMaxlength']);
var tpl = '<div ng-if="maxLength" ng-include="\'length.tpl.html\'"></div>' +
'<div ng-if="required" ng-include="\'required.tpl.html\'"></div>';
var el = $compile(tpl)(scope);
elem.after(el);
scope.$watch(attrs['ngModel'], function(newValue, oldValue, scope) {
if (ngModel.$error !== null && ngModel.$error.maxlength) {
scope.maxLength = true;
} else {
scope.maxLength = false;
}
if (ngModel.$error !== null && ngModel.$error.required && ngModel.$dirty) {
scope.required = true;
} else {
scope.required = false;
}
});
}
}
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
<script type="text/ng-template" id="length.tpl.html">
max length {{max}}
</script>
<script type="text/ng-template" id="required.tpl.html">
required
</script>
</head>
<body ng-controller="ctrl">
<h1>Input Validation</h1> {{title}}
<br><br>
<form name="form" novalidate>
<input dir name="input_one" ng-model="bar" ng-maxlength="CONSTANTS.LENGHT_1" required>
<br>
input one: {{form.input_one.$error}}
<br>
<br>
<input dir name="input_two" ng-model="foo" ng-maxlength="CONSTANTS.LENGHT_2">
</form>
<br>
input two: {{form.input_two.$error}}
</body>
</html>
On Plunkr
Hope it helps.
I think you're on the right track by using the form errors to toggle display. That's exactly how it's recommended in the standard Angular documentation.
If you'd like to show multiple errors for a single input, however, and possible even control the error messages from there, I'd recommend utilizing a service, such as implemented at http://plnkr.co/edit/iNcNs2ErrOnYf9I7whdu?p=preview.
Right now you can have one message per token, but as many tokens as you want per input. If you want multiple messages per token, just use an array of messages instead of single string value (note, unset does become more complicated with that method).
Hope that helps,
Alex

Resources