I'm creating a validation directive in angular and I need to add a tooltip to the element the directive is bound to.
Reading thru the web I found this solution setting a high priority and terminal to the directive, but since I'm using ngModel this doesn't work for me. This is what I'm doing right now:
return {
restrict: 'A',
require: 'ngModel',
replace: false,
terminal: true,
priority: 1000,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
element.attr('tooltip', '{{validationMessage}');
element.removeAttr("validator");
return {
post: function postLink(scope, element) {
$compile(element)(scope);
}
};
},
}
But it's not working for me. It throws the following error:
Error: [$compile:ctreq] Controller 'ngModel', required by directive 'validator', can't be found!
This is the HTML where I'm using the directive:
<input id="username" name="username" data-ng-model="user.username" type="text" class="form-control" validator="required, backendWatchUsername" placeholder="johndoe" tabindex="1" >
Any ideas on how can I solve this?
Thanks.
The reason is because of the combination of your directive priority with terminal option. It means that ngModel directive will not render at all. Since your directive priority (1000) is greater than ng-model's(0) and presence of terminal option will not render any other directive with lower priority (than 1000). So some possible options are :
remove the terminal option from your directive or
reduce the priority of your directive to 0 or -1 (to be less than or equal to ngModel) or
remove ng-model requirement from the directive and possibly use a 2-way binding say ngModel:"=" (based on what suits your requirement).
Instead of adding tooltip attribute and recompiling the element, you could use transclusion in your directive and have a directive template.
terminal - If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined). Note that expressions and other directives used in the directive's template will also be excluded from execution.
demo
angular.module('app', []).directive('validator', function($compile) {
return {
restrict: 'A',
require: 'ngModel',
replace: false,
terminal: true,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
element.attr('tooltip', '{{validationMessage}');
element.removeAttr("validator");
return {
post: function postLink(scope, element) {
$compile(element)(scope);
}
};
},
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<input validator ng-model="test">
</div>
As explained in my comments you do not need to recompile the element and all these stuffs, just set up an element and append it after the target element (in your specific case, the input).
Here is a modified version of validation directive (i have not implemented any validation specifics which i believe you should be able to wire up easily).
So what you need is to set up custom trigger for tooltip which you can do by using the $tooltipprovider. So set up an event pair when you want to show/hide tooltip.
.config(function($tooltipProvider){
$tooltipProvider.setTriggers({'show-validation':'hide-validation'});
});
And now in your directive just set up your tooltip element as you like with tooltip attributes on it. compile only the tooltip element and append it after the target element (you can manage positioning with css ofcourse). And when you have validation failure, just get the tooltip element reference (which is reference to the tooltip element, instead of copying the reference you could as well select every time using the selector) and do $tooltipEl.triggerHandler('show-validation') and to hide it $tooltipEl.triggerHandler('show-validation').
Sample Implementation which shows the tooltip after 2 sec and hides it after 5 sec (since validation is not in the scope of this question you should be able to wire it up):
.directive('validator', function($compile, $timeout){
var tooltiptemplate = '<span class="validation" tooltip="{{validationMessage}}" tooltip-trigger="show-validation" tooltip-placement="bottom"></span>';
var tooltipEvents = {true:'show-validation', false:'hide-validation'};
return {
restrict: 'A',
require: 'ngModel',
replace: false,
priority: 1000,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
return {
post: function postLink(scope, element) {
var $tooltipEl= getTooltip();
init();
function init(){
scope.$on('$destroy', destroy);
scope.validationMessage ="Whoops!!!";
$timeout(function(){
toggleValidationMessage(true);
},2000);
$timeout(function(){
toggleValidationMessage(false);
},5000);
}
function toggleValidationMessage(show){
$tooltipEl.triggerHandler(tooltipEvents[show]);
}
function getTooltip(){
var elm = $compile(angular.element(tooltiptemplate))(scope);
element.after(elm);
return elm;
}
function destroy(){
$tooltipEl= null;
}
}
};
},
}
});
Plnkr
Inline Demo
var app = angular.module('plunker', ['ui.bootstrap']);
app.controller('MainCtrl', function($scope) {
$scope.user = {
username: 'jack'
};
}).directive('validator', function($compile, $timeout) {
var tooltiptemplate = '<span class="validation" tooltip="{{model}}" tooltip-trigger="show-validation" tooltip-placement="bottom"></span>';
var tooltipEvents = {
true: 'show-validation',
false: 'hide-validation'
};
return {
restrict: 'A',
require: 'ngModel',
replace: false,
priority: 1000,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
return {
post: function postLink(scope, element) {
var $tooltipEl = getTooltip();
init();
function init() {
scope.$on('$destroy', destroy);
scope.validationMessage = "Whoops!!!";
$timeout(function() {
toggleValidationMessage(true);
}, 2000);
$timeout(function() {
toggleValidationMessage(false);
}, 5000);
}
function toggleValidationMessage(show) {
$tooltipEl.triggerHandler(tooltipEvents[show]);
}
function getTooltip() {
var elm = $compile(angular.element(tooltiptemplate))(scope);
element.after(elm);
return elm;
}
function destroy() {
elm = null;
}
}
};
},
}
}).config(function($tooltipProvider) {
$tooltipProvider.setTriggers({
'show-validation': 'hide-validation'
});
});
/* Put your css in here */
.validation {
display: block;
}
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<link data-require="bootstrap-css#3.1.*" data-semver="3.1.1" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
<script>
document.write('<base href="' + document.location + '" />');
</script>
<script data-require="angular.js#1.3.x" src="https://code.angularjs.org/1.3.12/angular.js" data-semver="1.3.12"></script>
<script data-require="ui-bootstrap#*" data-semver="0.12.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.min.js"></script>
</head>
<body ng-controller="MainCtrl">
<br/>
<br/>{{user.username}}
<input id="username" name="username" data-ng-model="user.username" type="text" class="form-control" validator="required, backendWatchUsername" placeholder="johndoe" tabindex="1">
</body>
</html>
You should not create a new isolated scope in your directive: this will mess up with the others directives (and in this case will not share ngModel).
return {
restrict: 'A',
require: 'ngModel',
compile: function compile(element, attrs) {
element.attr('tooltip', '{{validationMessage}');
element.removeAttr("validator");
return {
post: function postLink(scope, element) {
$compile(element)(scope);
}
};
},
}
I invite you to check the Angular-UI library and especially how they have implemented their ui.validate directive: http://angular-ui.github.io/ui-utils/
Related
Context
I m actually developping an application in which I need to generate directive dynamically from a controller to a view (drag n drop system).
It works like this :
the directive
angular.module('app')
.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamic, function (html) {
ele.html(html);
$compile(ele.contents())(scope);
});
}
};
});
And in the controller, it looks like :
$scope.listModules = [
{libelle: "Utilisateurs connectés", template: "<div user-connecte></div>", drag: true}
]
And in the HTML file :
<div ng-repeat="currentModule in listModules" dynamic="currentModule.template">
The directive loaded
</div>
The problem
I want to use the
$scope.$emit('event');
from my controller, to send some information to my directive, which is supposed to get it with :
$scope.$on('event',function(){console.log('yiihaaa');})
It seems that it doesnt happen anything...
I need the log to be displayed.
Can you help me ?
Thanks for advance
ngRepeate create own isolated scope, and scope in your directive link function is this isolated scope.
When you do $emit
Dispatches an event name upwards through the scope hierarchy notifying the registered $rootScope.Scope listeners.
For sending event to child scopes you need use $broadcast
So for solving your problem you have at least two ways
1) use $broadcast instead of $emit
$scope.rechercherStats = function () { $scope.$broadcast('reload'); };
// Code goes here
angular.module('app',[])
.controller('ctrl',function($scope){
$scope.listModules = [
{libelle: "Utilisateurs connectés", template: "<div user-connecte></div>", drag: true}
];
$scope.rechercherStats = function () {console.log('reload'); $scope.$broadcast('reload'); };
});
angular.module('app')
.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamic, function (html) {
ele.html(html);
$compile(ele.contents())(scope);
});
scope.$on('reload',function(){console.log('yiihaaa');})
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
Sample
<div ng-repeat="currentModule in listModules" dynamic="currentModule.template">
The directive loaded
</div>
<input type="button" ng-click="rechercherStats()" value="btn"/>
</div>
2) add listener not in scope ngRepeat, but in parent scope
angular.module('app')
.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamic, function (html) {
ele.html(html);
$compile(ele.contents())(scope);
});
scope.$parent.on('event',function(){console.log('yiihaaa');});
}
};
});
// Code goes here
angular.module('app',[])
.controller('ctrl',function($scope){
$scope.listModules = [
{libelle: "Utilisateurs connectés", template: "<div user-connecte></div>", drag: true}
];
$scope.rechercherStats = function () {console.log('reload'); $scope.$emit('reload'); };
});
angular.module('app')
.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamic, function (html) {
ele.html(html);
$compile(ele.contents())(scope);
});
scope.$parent.$on('reload',function(){console.log('yiihaaa');})
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
Sample
<div ng-repeat="currentModule in listModules" dynamic="currentModule.template">
The directive loaded
</div>
<input type="button" ng-click="rechercherStats()" value="btn"/>
</div>
As #Grundy said. Use $broadcast and $on from $rootScope.
var subscription = $rootScope.$on("myEvent", function() {
console.log("yiihao");
});
Don't forget to destroy it.
$rootScope.$on('$destroy', function () {
subscription();
});
The $broadcast would be like that.
$rootScope.$broadcast("myEvent", {});
Have you tried this link: http://onehungrymind.com/angularjs-dynamic-templates/
They are loading directives on the fly. The approach can be adapted for your requirements?
I have the following code:
<div id='parent'>
<div id='child1'>
<my-select></my-select>
</div>
<div id='child2'>
<my-input></my-input>
</div>
</div>
I also have two directives which get some data from the data factory. I need the two directives to talk to each other such that when a value in select box is changed the input in changes accordingly.
Here's my two directives:
.directive("mySelect", function ($compile) {
return {
restrict: 'E',
scope:'=',
template: " <select id='mapselectdropdown'>\
<option value=map1>map1</option> \
<option value=map2>map2</option> \
</select>'",
link: function (scope, element, attrs) {
scope.selectValue = //dont konw how to get the value of the select
}
};
})
.directive("myInput", function($compile) {
return {
restrict: 'E',
controller: ['$scope', 'dataService', function ($scope, dataService) {
dataService.getLocalData().then(function (data) {
$scope.masterData = data.input;
});
}],
template: "<input id='someInput'></input>",
link: function (scope, element, attrs) {
//here I need to get the select value and assign it to the input
}
};
})
This would essentially do the onchange() function that you can add on selects. any ideas?
You could use $rootScope to broadcast a message that the other controller listens for:
// Broadcast with
$rootScope.$broadcast('inputChange', 'new value');
// Subscribe with
$rootScope.$on('inputChange', function(newValue) { /* do something */ });
Read Angular docs here
Maybe transclude the directives to get access to properties of outer scope where you define the shared variable ?
What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.
-> https://docs.angularjs.org/guide/directive
After much research this is what worked...
I added the following:
.directive('onChange', function() {
return {
restrict: 'A',
scope:{'onChange':'=' },
link: function(scope, elm, attrs) {
scope.$watch('onChange', function(nVal) { elm.val(nVal); });
elm.bind('blur', function() {
var currentValue = elm.val();
if( scope.onChange !== currentValue ) {
scope.$apply(function() {
scope.onChange = currentValue;
});
}
});
}
};
})
Then on the element's link function I added:
link: function (scope, elm, attrs) {
scope.$watch('onChange', function (nVal) {
elm.val(nVal);
});
}
Last added the attribute that the values would get set to in the scope:
<select name="map-select2" on-change="mapId" >
I have an object on the scope of a controller that contains some presentation data for an input:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.settings = {
value: 'xxx',
required: true,
show: true,
readonly: false
};
});
In the actual application, this is part of a larger object loaded from the server. I created a directive that would take this presentation object as input and attach the necessary directives:
app.directive('fieldSettings',
[/*$injectables*/
function (/*$injectables*/) {
return {
priority: 100,
restrict: 'A',
scope: {
fieldSettings: '='
},
compile: function (el, attrs) {
return function (scope, iElement, iAttrs) {
iAttrs.$set('ng-model', 'fieldSettings.value');
iAttrs.$set('ng-show', 'fieldSettings.show');
iAttrs.$set('ng-required', 'fieldSettings.required');
iAttrs.$set('ng-readonly', 'fieldSettings.readonly');
}
}
};
}
]);
As this plunk demonstrates, the attributes are added but the logic is not being applied. According to the documentation for angular, the directives I am trying to apply have a priority of 0 and the input directive has a priority of 100. I set mine to 100 but this value seems to have no affect regardless of the value I choose for it.
I want
<input field-settings="settings" />
to behave like
<input ng-model="settings.value" ng-show="settings.show" ng-required="settings.required" ng-readonly="settings.readonly" />
but literally be
<input ng-model="fieldSettings.value" ng-show="fieldSettings.show" ng-required="fieldSettings.required" ng-readonly="fieldSettings.readonly" />
where fieldSettings is the directive's local scope variable bound to the MaintCtrl's local scope variable settings.
Just adding the attributes without compiling won't do anything.
My similar answeres:
creating a new directive with angularjs
How to get ng-class with $dirty working in a directive?
Angular directive how to add an attribute to the element?
Here is a plunker: http://plnkr.co/edit/8kno6iwp3hH5CJFQt3ql?p=preview
Working directive:
app.directive('fieldSettings',
['$compile',
function ($compile) {
return {
restrict: 'A',
scope: {
fieldSettings: '='
},
priority: 1001,
terminal: true,
compile: function(tElm,tAttrs){
tElm.removeAttr('field-settings')
tElm.attr('ng-model', 'fieldSettings.value');
tElm.attr('ng-show', 'fieldSettings.show');
tElm.attr('ng-required', 'fieldSettings.required');
tElm.attr('ng-readonly', 'fieldSettings.readonly');
var fn = $compile(tElm);
return function(scope){
fn(scope);
}
}
};
}
I have an angular directive which is initialized like so:
<conversation style="height:300px" type="convo" type-id="{{some_prop}}"></conversation>
I'd like it to be smart enough to refresh the directive when $scope.some_prop changes, as that implies it should show completely different content.
I have tested it as it is and nothing happens, the linking function doesn't even get called when $scope.some_prop changes. Is there a way to make this happen ?
Link function only gets called once, so it would not directly do what you are expecting. You need to use angular $watch to watch a model variable.
This watch needs to be setup in the link function.
If you use isolated scope for directive then the scope would be
scope :{typeId:'#' }
In your link function then you add a watch like
link: function(scope, element, attrs) {
scope.$watch("typeId",function(newValue,oldValue) {
//This gets called when data changes.
});
}
If you are not using isolated scope use watch on some_prop
What you're trying to do is to monitor the property of attribute in directive. You can watch the property of attribute changes using $observe() as follows:
angular.module('myApp').directive('conversation', function() {
return {
restrict: 'E',
replace: true,
compile: function(tElement, attr) {
attr.$observe('typeId', function(data) {
console.log("Updated data ", data);
}, true);
}
};
});
Keep in mind that I used the 'compile' function in the directive here because you haven't mentioned if you have any models and whether this is performance sensitive.
If you have models, you need to change the 'compile' function to 'link' or use 'controller' and to monitor the property of a model changes, you should use $watch(), and take of the angular {{}} brackets from the property, example:
<conversation style="height:300px" type="convo" type-id="some_prop"></conversation>
And in the directive:
angular.module('myApp').directive('conversation', function() {
return {
scope: {
typeId: '=',
},
link: function(scope, elm, attr) {
scope.$watch('typeId', function(newValue, oldValue) {
if (newValue !== oldValue) {
// You actions here
console.log("I got the new value! ", newValue);
}
}, true);
}
};
});
I hope this will help reloading/refreshing directive on value from parent scope
<html>
<head>
<!-- version 1.4.5 -->
<script src="angular.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<my-test reload-on="update"></my-test><br>
<button ng-click="update = update+1;">update {{update}}</button>
</body>
<script>
var app = angular.module('app', [])
app.controller('Ctrl', function($scope) {
$scope.update = 0;
});
app.directive('myTest', function() {
return {
restrict: 'AE',
scope: {
reloadOn: '='
},
controller: function($scope) {
$scope.$watch('reloadOn', function(newVal, oldVal) {
// all directive code here
console.log("Reloaded successfully......" + $scope.reloadOn);
});
},
template: '<span> {{reloadOn}} </span>'
}
});
</script>
</html>
angular.module('app').directive('conversation', function() {
return {
restrict: 'E',
link: function ($scope, $elm, $attr) {
$scope.$watch("some_prop", function (newValue, oldValue) {
var typeId = $attr.type-id;
// Your logic.
});
}
};
}
If You're under AngularJS 1.5.3 or newer, You should consider to move to components instead of directives.
Those works very similar to directives but with some very useful additional feautures, such as $onChanges(changesObj), one of the lifecycle hook, that will be called whenever one-way bindings are updated.
app.component('conversation ', {
bindings: {
type: '#',
typeId: '='
},
controller: function() {
this.$onChanges = function(changes) {
// check if your specific property has changed
// that because $onChanges is fired whenever each property is changed from you parent ctrl
if(!!changes.typeId){
refreshYourComponent();
}
};
},
templateUrl: 'conversation .html'
});
Here's the docs for deepen into components.
Have the following problem. I want to make two directives. One of them will be an attribute for another.
Something like this.
<html>
<title>Directives</title>
<head lang="en">
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js" type="text/javascript"></script>
<script src="main.js"></script>
</head>
<body ng-app="app">
<outer inner></outer>
</body>
</html>
The directive source code is here:
var app = angular.module('app', []);
app.directive('inner', function() {
return {
require: "^ngModel",
restrict: "AC",
transclude: true,
replace: false,
templateUrl: /* here is a path to template it's not interesting*/,
controller: function($scope) {
console.log('controller...');
},
link: function(scope, element, attrs) {
console.log('link...');
}
};
});
app.directive('outer', function($q, $rootScope) {
return {
require: "^ngModel",
restrict: "E",
replace: true,
scope: { /* isolated scope */ },
controller: function($scope) {},
templateUrl: /* path to template */,
link: function (scope, elem, attrs, ctrl) {}
}
});
The problem is that controller of outer works, but inner doesn't... Neither link nor controller function works... Can't understand what is wrong...
Any ideas?
The reason its not working is because both directives have been asked to render a template on the same element, and its ambiguous as to which one should be given priority.
You can fix this by giving the inner directive priority over the outer directive (higher numbers indicate higher priority).
Inner:
app.directive('inner', function() {
return {
priority:2,
restrict: "AC",
transclude: true,
replace: false,
template: "<div>{{say()}}<span ng-transclude/></div>",
controller: function($scope) {
$scope.message = "";
$scope.say = function() {
return "this is message";
};
// $scope.say(); // this doesn't work as well
console.log('controller...');
},
link: function(scope, element, attrs) {
// alert('hey');
// console.log('link...');
}
};
});
Also, both directives cannot transclude their contents. One must be 'transclude:false' and the other must be transclude:true.
app.directive('outer', function($q, $rootScope) {
return {
priority:1,
restrict: "E",
transclude:false,
scope: { /* isolated scope */ },
controller: function($scope) {
$scope.message = "";
$scope.sayAgain = function() {
return "one more message";
};
$scope.sayAgain(); // this doesn't work as well
},
template: "<div>{{sayAgain()}}</div>",
link: function (scope, elem, attrs, ctrl) {}
}
});
Here is a working fiddle:
JSFiddle