I'm creating a smart input directive which will wrap a text input element and it requires access to the model on the input element in order to manipulate some classes on the directive.
Because the input element can be one of many types (text, email, password), I need to transclude the directive and be able to add different types of validation for each.
The problem I'm having (as with many others on the Internet), is scope inheritance.
Here's what my current code looks like
HTML
<smart-input ng-model="username">
<span ng-show="isTyping">{{ placeholder }}</span>
<input type="text" name="username" ng-model="username" ng-minlength="4" ng-maxlength="20" required />
</smart-input>
JS
angular.module('myApp').directive('smartInput', function ($compile) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
model: '=ngModel'
},
template: '<div class="text-input" ng-class="{typing: isTyping}" ng-transclude>' +
'</div>',
link: function(scope, element, attrs) {
scope.isTyping = false;
scope.$watch('model', function(value) {
console.log(value);
scope.isTyping = value.length > 0;
});
}
};
});
Basically, the value inside the $watch function is undefined so obviously I'm not doing this correctly.
So, how can I bind a model to the input field, while have the directive have a reference to the same object and be able to watch it's value?
When you use isolate scope with transclusion, your scopes do not have parent/child relationship. It looks like this:
<controllerScope>
<smartInputScope>
<transcludedContentScope>
That's why in order to access smartInputScope's model property, we have to access $$prevSibling.model In your first example ng-model="username" works because this scope inherits from controllerScope, it's accessing a parent's scope property.
Check out my solution with custom transclusion: http://plnkr.co/edit/cV9urKJdcn4mKlpqPJTr?p=preview
app.directive('smartInput', function($compile) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
model: '=ngModel'
},
template: '<div class="text-input" ng-class="{typing: isTyping}">' +
'</div>',
compile: function(element, attr, linker) {
return {
pre: function(scope, element, attr) {
linker(scope, function(clone) { //bind the scope your self
element.append(clone); // add to DOM
});
},
post: function postLink(scope, iElement, iAttrs) {
scope.isTyping = false;
scope.$watch('model', function(value) {
console.log(value);
scope.isTyping = value.length > 0;
});
}
};
}
};
});
In the html, I don't need $$prevSibling anymore:
<input type="text" name="username" ng-model="model" ng-minlength="4" ng-maxlength="20" required />
Related
I want to make a directive which take a class name conditionally. However, I found that my code can work only if I hardcode the class name into the class attribute. If I try to use it with any expression, its failed to work.
For e.g.
// HTML
// Doesn't work (cannot find class="editable" in the final output template)
<tit-txt ng-class="true ? 'editable' : ''" ng-model="mdEnt.phone"></tit-txt>
// Works! (I can find class="editable" in the final output template)
<tit-txt class="editable" ng-model="mdEnt.phone"></tit-txt>
//JS
.directive('titTxt', function () {
return {
restrict: 'E',
scope: {
ngModel: '=',
},
link: function (scope, element, attrs) {
scope.editable = element.hasClass('editable') ? 'editable' : '';
},
template: '<input ng-class="editable" ng-model="ngModel" />',
};
})
Anyone can explain to me that why is it happening? How can I use it with expression?
UPDATE 1
// HTML
// Doesn't work
<tit-txt ng-class="{'editable': true}" ng-model="mdEnt.phone"></tit-txt>
//JS
.directive('titTxt', function () {
return {
restrict: 'E',
scope: {
title: '#',
fieldName: '#',
ngModel: '=',
},
link: function (scope, element, attrs) {
console.log(element.hasClass('editable'));
scope.editable = element.hasClass('editable') ? 'editable' : '';
},
template: '<div><span>{{title}}: </span><input id="{{fieldName}}" ng-class="{editable: true}" name="{{fieldName}}" ng-model="ngModel" /></div>',
};
})
Anyone can explain to me that why I get false in the console.log(element.hasClass('editable'));?
With the class attribute, the element's class is set by JavaScript. With the ng-class directive, the class is set by the AngularJS framework. When there are more that one directive on an element, there is no guarantee of the order of execution of the code of the respective directives.
Avoid having AngularJS manipulate the DOM and having subsequent AngularJS manipulate the model based on the state of the DOM. With MVC frameworks the Model should be the single source of truth and the DOM should be directly determined by the Model.
<tit-txt inner-class="true ? 'editable' : ''" my-model="mdEnt.phone">
</tit-txt>
app.directive('titTxt', function () {
return {
restrict: 'E',
scope: {
title: '#',
fieldName: '#',
innerClass: '<',
myModel: '=',
},
link: function (scope, element, attrs) {
scope.$watch(attrs.innerClass, function(newValue) {
console.log("inner-class=", newValue);
});
},
template: `<div><span>{{title}}: </span>
<input id="{{fieldName}}" ng-class="innerClass"
name="{{fieldName}}" ng-model="myModel" />
</div>`,
};
})
Notice how the directive uses one-way, '<', binding to compute the value of the inner-class attribute from an AngularJS Expression.
Also notice that I changed ng-model to my-model. The ng- prefix is reserved for core AngularJS directives. Use of ng-model should be specifically be avoided unless the custom directive properly integrates with the ngModelController.
plunker for code
There are two directive link with a element.
HTML
<div class="row">
<div class="form-group">
<label class="col-sm-5 control-label" for="VehicleOriginalCost">{{questions[$state.current.name].VehicleOriginalCost.QuestionData._text}}</label>
<div class="col-sm-6">
<input type="text" id="VehicleOriginalCost" class="form-control" name="VehicleOriginalCost" ng-model="answers.VehicleOriginalCost" data-que-obj="questions[$state.current.name].VehicleOriginalCost.QuestionData" required text-control-dir cbq-dir number-only>
</div>
</div>
<span class="form-error" ng-show="submitted && DTOstep1.VehicleOriginalCost.$error.required">This field is required.</span>
</div>
Directive are textControlDir and cbqDir.
selectControlDir directive sets value for the questions if there is any default value.
cbqDir directive should access updated value after selectControlDir and do operation.
But cbqDir do not have updated value.
I am getting undefined in cbq directory when I pass ngModle in url.
Directive
function textControlDir()
{
return {
transclude: true,
restrict: 'A',
priority: 100,
scope: {
ngModel: '=',
queObj: '='
},
link: function (scope, element, attrs) {
if (angular.isUndefined(scope.ngModel))
{
scope.ngModel = scope.queObj._pageAttributes.defaultValue;
}
}
};
}
function cbqControlDir($http)
{
return {
transclude: true,
restrict: 'A',
priority: 200,
require: ngModel,
link: function (scope, element, attrs) {
//http call
//url apc.php/ngModel
}
};
}
I tried to solve this using priority but no change.
please help me to find out how to get updated scope value in other directive.
If I remove selectControlDir, it works fine. But i don't want to remove, looking for solution how can i refresh/update scope here.
Issue is only on load no ton change. onchange it works fine because value is available in $scope.
I am writing a directive in AngularJs and I want to pass ng-model as an argument.
<div class="col-md-7"><time-picker></time-picker></div>
The directive is:
app.directive('timePicker', function () {
return {
restrict: 'E',
replace: true,
template: '<input type="text" class="form-control time-picker" ng-model="emp.signin">',
link: function ($scope, element, form) {
$(element).timepicker({'timeFormat': 'H:i:s'});
}
}
})
It is working fine, and here the ng-model is emp.signin. I want to be able to pass this ng-model dynamically as argument
How is this possible?
You can use
<div class="col-md-7"><time-picker model-value="emp.signin"></time-picker></div>
Angular
app.directive('timePicker', function () {
return {
restrict: 'E',
replace: true,
template: '<input type="text" class="form-control time-picker"ng-model="modelValue ">',
scope: {
modelValue : '=',
}
link: function ($scope, element, form) {
$(element).timepicker({'timeFormat': 'H:i:s'});
}
}
})
Explaination
The “=” prefix will create a two-way binding between the parent and
directive scope and it’ll always expect the attribute value to be the
model name which means you cannot provide an expression as the value
of attribute mapped to “=” prefix.
For reference: "http://www.undefinednull.com/2014/02/11/mastering-the-scope-of-a-directive-in-angularjs/"
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/
i was trying to watch a scope variable inside a directives controller which i injected via template.But the listener never seems to trigger.
http://plnkr.co/edit/a9UWiVZlKhaKf0Z0utSG?p=preview
var angularSimpleAuth = angular.module('angularSimpleAuth', []);
angularSimpleAuth.directive('simpleAuthUsername',function(){
return {
restrict: 'A',
scope:{},
template:'<input type="text" ng-model="userName" />',
controller:function($scope){
$scope.$watch('userName',function(val){
console.log('Value'+val);
});
},
link: function($scope, elem, attrs,controllers) {
console.log('In link for username');
}
};
})
Could anybody tell me what I'm doing wrong here.Any help appreciated
Thanks
The problem was you have the directive applied on input element. Since the directive is rendering an input element already, it has either to be marked as a element or applied with a attribute replace true.
Change your HTML to:
<simple-auth-username></simple-auth-username>
And the directive to:
angularSimpleAuth.directive('simpleAuthUsername',function(){
return {
restrict: 'E',
scope:{},
template:'<input type="text" ng-model="userName" />',
controller:function($scope){
$scope.$watch('userName',function(val){
console.log('Value'+val);
});
},
link: function($scope, elem, attrs,controllers) {
console.log('In link for username');
}
};
})