What is the angular way of binding a directive inside another directive? - javascript

I am writing a directive which will be used to establish client and server-side validation on an input. It should accept an array of validator names (e.g. aa-validate="required,unique"), loop through them, add client-side validation directives for all possible validators (e.g. required should add ngRequired), and for the rest, post to a server-side validation API.
The last part of that works well: I am watching the ngModel attribute, and posting to the server with a 100ms timeout. However, setting client-side validation directives from within the linking function of my directive does NOT cause them to be compiled and linked. In other words, they do nothing. Here is my code:
angular.module('form', [])
.directive('aaValidate', ['$http', function($http) {
return {
priority: 1,
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
var validate = attrs.aaValidate,
validators = validate.split(',');
// This is the problem!
//
// Populate possible client-side validators
for (var i = 0, len = validators.length; i < len; i++) {
var validator = validators[i];
switch (validator) {
case 'required':
attrs.$set('ngRequired', 'true'); break;
// ... and so on for ngPattern, etc.
default: break;
}
}
scope.$watch(attrs.ngModel, function(value) {
// This part works!
//
// Clear existing timeout, reset it with an
// $http.post to my validation API, the result is
// passed into ctrl.$setValidity
});
}
}
}]);
I did make an attempt to inject $compile, and re-compile the element at the end of the linking function. I ended up with infinite recursion, likely because I failed to remove some attributes, but even if I manage to do it this way, it feels rather ugly. What is the correct approach?
Any help is greatly appreciated. Thanks in advance.
EDIT: jsFiddle: http://jsfiddle.net/3nUdj/4/

My first answer was wrong - I don't think there's any way around using the $compile service. Here's how you can do it without getting infinite recursion. I basically split the directive in two directives - one adds the validation directives, removes itself and recompiles. The other does the other stuff:
angular.module('form', [])
.directive('aaValidate', ['$http', '$compile', function ($http, $compile) {
return {
link: function (scope, element, attrs) {
var validate = attrs.aaValidate,
validators = validate.split(',');
// Populate possible front-end validators
for (var i = 0, len = validators.length; i < len; i++) {
var validator = validators[i];
switch (validator) {
case 'required':
attrs.$set('ngRequired', 'true');
break;
default:
break;
}
}
attrs.$set('aaOther', '');
element.removeAttr('aa-validate');
$compile(element)(scope);
}
}
}])
.directive('aaOther', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
scope.$watch(attrs.ngModel, function (value) {
// Server-side validation
});
}
}
});
You have to recompile the linked element for this to work in ng-repeat. I've updated the fiddle: http://jsfiddle.net/3nUdj/7/

Related

AngularJS notation in input type range min attribute

I would expect the following expression to have the same outcome:
Case 1:
<input type="range" name="myRangeInput" ng-model="value.rangeInput" value="value.rangeInput" min="-55" max="55">
Case 2 (difference to case 1 is that I replaced 55 with AngularJS scope variables):
<input type="range" name="myRangeInput" ng-model="value.rangeInput" value="value.rangeInput" min="{{value.rangeInputMin}}" max="{{value.rangeInputMax}}">
with value.rangeInputMax equals 55 and value.rangeInputMin equals -55.
But they do not have the same output. For example let's says value.rangeInput is in both cases -10. Then in the 1st example the dot in the range slider is set at -10. But in the 2nd example the dot is set to 0.
I tried to convert value.rangeInputMin and value.rangeInputMax into numbers and change the statement (without double quotes) to this:
<input type="range" name="myRangeInput" ng-model="value.rangeInput" value="value.rangeInput" min={{value.rangeInputMin}} max={{value.rangeInputMax}}>
I also tried with different notations, e.g. value.rangeInputMin, "value.rangeInputMin", tried to set it with ng-init, create another scope variable and assign the value in this one, etc.
But it is still showing a different behaviour than in the 1st case.
Per my comments above, I think you've found a bug as I'd expect to be able to declaratively set this value in your template alongside your min and max values. This simply isn't the case. A typical workaround for this is to set the model value after you've set your min and max values using $timeout. It's not ideal but it works.
controller function
function($timeout) {
var vc = this
// vc.rangeInput = -10;
vc.rangeInputMin = -55;
vc.rangeInputMax = 55;
$timeout(function(){
vc.rangeInput = -10;
})
}
You can see it working here - http://codepen.io/jusopi/pen/vLQKJY?editors=1010
If you need to, you can write a simple directive to basically trigger ng-init-like functionality on the next $digest cycle. This might be a better solution if you run into this issue more than once of twice in your design.
callLater directive
.directive('callLater', [
'$timeout',
function($timeout) {
return {
restrict: 'A',
scope: {
callLater: '&'
},
link: function($scope, elem, attr) {
$timeout(function(){
$scope.callLater()
})
}
}
}
])
directive in template
<input type="range" name="myRangeInput" ng-model="vc.delayedInput"
min="{{vc.rangeInputMin || -55}}" max="{{vc.rangeInputMax || 55}}"
call-later="vc.delayedInput = -10;">
example - http://codepen.io/jusopi/pen/JGeKOz?editors=1010
The problem is commented here: https://github.com/driftyco/ionic/issues/1948
JWGmeligMeyling created the ngMax and ngMin directives and they seem to work pretty well:
.directive('ngMin', function() {
return {
restrict : 'A',
require : ['ngModel'],
compile: function($element, $attr) {
return function linkDateTimeSelect(scope, element, attrs, controllers) {
var ngModelController = controllers[0];
scope.$watch($attr.ngMin, function watchNgMin(value) {
element.attr('min', value);
ngModelController.$render();
})
}
}
}
})
.directive('ngMax', function() {
return {
restrict : 'A',
require : ['ngModel'],
compile: function($element, $attr) {
return function linkDateTimeSelect(scope, element, attrs, controllers) {
var ngModelController = controllers[0];
scope.$watch($attr.ngMax, function watchNgMax(value) {
element.attr('max', value);
ngModelController.$render();
})
}
}
}
})
Here's the codepen: http://codepen.io/anon/pen/MKzezB
Try removing value="value.rangeInput" from the markup.

Angular directives settings pattern

I'm looking for a good design pattern to provide angular directives render acording to some global specified parametrs.
For example, I have some factory called "Settings", that holds the value "directiveColor: red".
When i do the link in my directive, I ask the Settings about my directiveColor value. Everything is working fine - I got red and put element on the page. But I have hundreds of this elements on the page, and every directive before render ask for settings... I think it's not very good way.
What will you recomend?
UPD
factory
app.factory('Settings', function() {
var data = {
//...
directiveColor: red //set by user
}
//...
GetSettings : function () {return data}
}
directve
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
link: function(scope, elem, attr) {
scope.data = {
//...
color: Settings.GetSettings().directiveColor
};
};
}]);
//later "color" used in template through the scope
That's how it works for now (works fine). But every time, when I render directive (many many times on the page, ngRepeat for table data), my directive ask for Settings to choose its color. I think, it is not good. Or maybe not?
There are two considerations here. First, you are right that it is not optimal, and directive actually provides a way to do that call once, read about Compile-PreLink-PostLink in angular directives. Basically you want this call in Compile step if it is the same for all directives in your app.
Second consideration is that Settings.GetSettings().directiveColor will give really really small overhead if GetSettings() returns just an object that you only create once ( and that is what happened as angular factories are singletons )
In your case you can do
app.factory('Settings', function() {
var data = {
directiveColor: 'red' //set by user
}
return {
GetSettings : function () {return data}
}
})
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
compile: function(elem, attrs) {
var color = Settings.GetSettings().directiveColor
return function postLink(scope, elem, attr) {
scope.data = {
color: color
};
}
}
}
}])
instead of declaring link property on directive.

Custom validators with AngularJs

I'm writing my own custom AngularJs validators which look like this:
.directive('float', function ($log) {
return {
restrict: 'A',
require: 'ngModel',
scope: {float: '='},
link: function ($scope, ele, attrs, ctrl) {
var settings = $scope.float || {};
ctrl.$validators.float = function(value) {
var valid = isTheInputValidFunction( settings );
ctrl.$setValidity('float', valid);
return valid;
};
}
};
});
I'm using the validators like so:
<input type="text"ng-model="someVar" name="preis" float="{precision: 5, scale: 2}">
However, as soon as I attach multiple validators, I get the following error:
Multiple directives [...] asking for new/isolated scope
This is, because all my validators get a settings-object which has to be passed into the scope scope: {float: '='}.
I know that I can use var settings = JSON.parse(attrs.float); in the directives, but it doesn't look right.
So my question is:
How do you correctly implement custom validators in AngularJs?
It really depends on whether you expect the settings to change.
If you think it will be constant, like in the example you've shown, then simply parsing once the value will be enough. The appropriate service to use in such a case is $parse:
link: function ($scope, ele, attrs, ctrl) {
var settings = $parse(attrs.float)($scope);
// …
}
If you think it may be used with a variable, you should watch its content:
link: function ($scope, ele, attrs, ctrl) {
var settings = undefined;
$scope.$watch(attrs.float, function (newSettings) {
settings = newSettings;
});
// …
}
Perhaps it is because you are calling $setValidity. I beleive the whole point of the $validators pipeline was to do it for you. Just return a boolean.
ctrl.$validators.float = function(value) {
return isTheInputValidFunction( settings );
};

passing defined object to another custom directive angularjs

so I have this custom directives that you could see below.
myApp.directive('myDirective', function (testService) {
return {
restrict:'EA',
link:function (scope, element, attr) {
//defined the object
var object = new object();
testService.setObject(object);
}
}
});
myApp.directive('mySecondDirective', function (testService) {
return {
restrict:'EA',
link:function (scope, element, attr) {
//call the variable from previous custom directive
console.log(testService.getobject()); -> always return undefined
}
}
});
and this is the html structure where I used the directives above.
<my-directive></my-directive>
<my-second-directive></my-second-directive>
there I want to retreive the object that contains new object() from previous custom directive, but it always return an undefined I wonder how could I do this without using require nor isolated scope as well.. could you guys help me ?
UPDATE
I create a service to provide the facility to set and retreive the object and apparently it returned undefined because I set the custom direcitve this way
<my-second-directive></my-second-directive>
<my-directive></my-directive>
and this is the service
define(
['services/services'],
function(services)
{
'use strict';
services.factory('testService', [function() {
var me = this;
var testObject = '';
return {
setObject: function(object) {
me.testObject = object;
},
getObject: function() {
return me.testObject;
}
}
}
]);
}
);
the thing is that I actually set the html markup like I already mentioned above which is
<my-second-directive></my-second-directive>
<my-directive></my-directive>
so could you give me some advice on how should I do this ?
note* that the passing object actually worked I prefer using services because it will easy to mantain latter. The question is how do I make the object accessible from another directive even though the initiate install of the object (set the object) in the directive that I defined at the html markup, as the last position of the html it self ?
UPDATE this is the PLUNKER that I've been made for you to understand the question it self
You could achieve this by firing a custom event and then listening for it in the second directive. Here's an updated plunker: http://plnkr.co/edit/512gi6mfepyc04JKfiep?p=info
Broadcast the event from the first directive:
app.directive('myDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
var object = {};
testService.setObject(object);
console.log('setting object');
scope.$broadcast('objectSet');
}
}
});
... and then listen for it on the second:
app.directive('mySecondDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
scope.$on('objectSet', function(){
console.log('retrieving object', testService.getObject());
})
}
}
});
You could also pass data along with the event, if you wanted to emit a specific piece of data to be picked up by the second directive.
1). Scope. Since you don't want to use controllers and isolated scope, then you can simply set this object as scope property.
myApp.directive('myDirective', function() {
return {
restrict: 'EA',
link: function(scope, element, attr) {
var object = {};
object.test = 21;
// set object as scope property
scope.object = object;
}
}
});
myApp.directive('mySecondDirective', function() {
return {
priority: 1, // priority is important here
restrict: 'EA',
link: function(scope, element, attr) {
console.log('Retrieve: ', scope.object);
}
}
});
Just make sure you are also defining priority on the second directive (only if both directive a applied to the same element) to make sure it's evaluated in proper turn (should be bigger then the one of myDirective, if omitted it's 0).
2). Service. Another simple solution is to use service. You can inject custom service into both directives and use it storage for you shared object.
Expanding from what #gmartellino said.
Anything that you wanted to do after listening to the event in second directive, can have a callBack method and use it.
app.directive('mySecondDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
// what if I created like this ?
// define the test variable
var test;
scope.$on('objectSet', function(){
//set the variable
test = testService.getObject();
console.log('retrieving object : ', testService.getObject());
//Anything that you wanted to do
//after listening to the event
//Write a callBack method and call it
codeToExecuteAsCallBack();
})
//then use this method to make a call back from the event
//and outside the event too.
var codeToExecuteAsCallBack = function(){
console.log(test);
}
codeToExecuteAsCallBack();
}
}
});
Updated plnkr link

Manually applying the ngModel directive

My directive needs to use ngModel.
I need to do this dynamically from within another directive as I want to do some funky stuff with scopes and abstract this away from the person writing the HTML.
My first thought was to use the $set function provided by the attrs argument in the link function, that works to modify the HTML but the directive itself does not get compiled. We can then combine this with the $compile provider, and it works.
attrs.$set('ngModel', someVar);
$compile(element)(scope);
The problem is that this creates infinite recursion if I do not (and I can not) replace the elements tag as the directive gets reapplied and recompiled indefinitely.
However I can fiddle with the priorities and get that to work:
module.directive('input', [
'$compile',
function($compile) {
return {
restrict: 'E',
scope: {},
priority: 100, // Set this high enough to perform other directives
terminal: true, // Make sure this is the last directive parsed
link: function(scope, element, attrs) {
var key = 'example';
attrs.$set('ngModel', key);
$compile(element, null, 100)(scope);
}
};
}
]);
This works fine, but it just feels wrong:
I now have to ensure that all other directives on the element are
capable of being recompiled as they will all get compiled twice.
I have to make sure that nobody uses a higher priority.
So this got me thinking why can't I just inject the ngModelDirective and force compile it against my element?
module.directive('input', [
'ngModelDirective',
function(ngModel) {
return {
restrict: 'E',
scope: {},
priority: 100, // Set this high enough to perform other directives
terminal: true, // Make sure this is the last directive parsed
require: '?^form',
link: function(scope, element, attrs, formCtrl) {
var key = 'example';
attrs.$set('ngModel', key);
var ngModelFactory = ngModel[0];
var ngModelLink = ngModelFactory.compile(element);
ngModelLink.call(this, scope, element, attrs, [ngModelFactory.controller, formCtrl]);
}
};
}
]);
See: https://github.com/angular/angular.js/blob/v1.2.x/src/ng/directive/input.js#L1356
No errors thrown, but nothing happens. It seems this isn't enough to hook it up, so my question is can anyone elaborate on to what I need to do link the ngModelDirective to my custom directive without forcing a recompile?
ngModel seems a bad fit for what you are trying to do. But you don't need it anyway. You can two-way-bind some variable and pass the name into the model directive scope:
app.directive("myDirective", function() {
// ...
scope: {
myModel = "=",
modelName = "myModel"
// ...
}
// ...
});
app.directive("ngModelDirective", function() {
// ...
// ...
transclude: true,
link: function(scope, element, attrs) {
var modelName = scope.modelName;
console.assert(modelName, '`modelName` must be set when using `ngModelDirective`.');
// TODO: Check if `scope[modelName]` is actually bound
doSomethingFancyWith(scope, modelName);
}
});
Template example:
<myDirective ngModelDirective my-model="..." />
Note that doSomethingFancyWith can read and write the model variable, with bindings to the outside world.
I don't think it is possible without a re-compile.
The ngModel is designed to be a kind of collaborator between other directives in the same element and also parent form diretives. For example, during complilation:
other directives (e.g. input, required or ng-change) may add its own $parser or $formatter to ngModel.
ngModel will add itself to a parent form directive if exists.
Therefore, if the ngModel is somehow added after the complication process is ended already, the above two actions will be missing.
Edit: In case the value to be assigned to ng-model attribute is known at the compile time, it is possible and will be something like this:
app.directive('myNgModel', function($compile) {
return {
restrict: 'A',
replace: false,
priority: 1000,
terminal: true, // these terminal and priority will stop all other directive from being compiled.
link: function (scope, element, attrs) {
var key = 'example';
attrs.$set('ngModel', key);
attrs.$set('myNgModel', null); // remove itself to avoid a recusion
$compile(element)(scope); // start compiling other directives
}
};
});
Here is the plunker with example: http://plnkr.co/edit/S2ZkiVIyq2bOK04vAnFO?p=preview
I've managed to do it. It's not the prettiest thing but it works and I can hook up my input directive to work using the native inputDirective so that it can use things like require or validate specific input types.
To build this against another standard directive that implements specific ngModel functionality such as ngChange just replace the injected inputDirective with the correct directive e.g., ngChangeDirective.
module.directive('input', function() {
return {
restrict: 'E',
scope: {},
require: '?ngModel',
priority: -1,
link: function(scope, element, attrs, ngModel) {
var key = 'example.property';
if (ngModel === undefined) {
attrs.$set('ngModel', key);
angular.injector(['ng']).invoke([
'inputDirective',
'ngModelDirective',
'$controller',
'$exceptionHandler',
'$parse',
'$animate',
function(inputDirective, ngModelDirective, $controller, $exceptionHandler, $parse, $animate) {
var ngModelFactory = ngModelDirective[0];
var ngModelLink = ngModelFactory.compile(scope); // Get the ngModel linkage function against this scope
ngModel = $controller(ngModelFactory.controller, {
$scope: scope,
$exceptionHandler: $exceptionHandler,
$attrs: attrs,
$element: element,
$parse: $parse,
$animate: $animate
}); // Call the ngModel controller and bootstrap it's arguments
// Call the inputDirective linkage function to set up the ngModel against this input
inputDirective[0].link(scope, element, attrs, ngModel);
element.data('$ngModelController', ngModel); // Allow additional directives to require ngModel on this element.
}
]);
}
}
};
});
NOTE: This will not work for ngOptions as it specifies terminal: true.

Categories

Resources