How to get evaluated attributes inside a custom directive - javascript

I'm trying to get an evaluated attribute from my custom directive, but I can't find the right way of doing it.
I've created this jsFiddle to elaborate.
<div ng-controller="MyCtrl">
<input my-directive value="123">
<input my-directive value="{{1+1}}">
</div>
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
element.val("value = "+attr.value);
}
});
What am I missing?

Notice: I do update this answer as I find better solutions. I also keep the old answers for future reference as long as they remain related. Latest and best answer comes first.
Better answer:
Directives in angularjs are very powerful, but it takes time to comprehend which processes lie behind them.
While creating directives, angularjs allows you to create an isolated scope with some bindings to the parent scope. These bindings are specified by the attribute you attach the element in DOM and how you define scope property in the directive definition object.
There are 3 types of binding options which you can define in scope and you write those as prefixes related attribute.
angular.module("myApp", []).directive("myDirective", function () {
return {
restrict: "A",
scope: {
text: "#myText",
twoWayBind: "=myTwoWayBind",
oneWayBind: "&myOneWayBind"
}
};
}).controller("myController", function ($scope) {
$scope.foo = {name: "Umur"};
$scope.bar = "qwe";
});
HTML
<div ng-controller="myController">
<div my-directive my-text="hello {{ bar }}" my-two-way-bind="foo" my-one-way-bind="bar">
</div>
</div>
In that case, in the scope of directive (whether it's in linking function or controller), we can access these properties like this:
/* Directive scope */
in: $scope.text
out: "hello qwe"
// this would automatically update the changes of value in digest
// this is always string as dom attributes values are always strings
in: $scope.twoWayBind
out: {name:"Umur"}
// this would automatically update the changes of value in digest
// changes in this will be reflected in parent scope
// in directive's scope
in: $scope.twoWayBind.name = "John"
//in parent scope
in: $scope.foo.name
out: "John"
in: $scope.oneWayBind() // notice the function call, this binding is read only
out: "qwe"
// any changes here will not reflect in parent, as this only a getter .
"Still OK" Answer:
Since this answer got accepted, but has some issues, I'm going to update it to a better one. Apparently, $parse is a service which does not lie in properties of the current scope, which means it only takes angular expressions and cannot reach scope.
{{,}} expressions are compiled while angularjs initiating which means when we try to access them in our directives postlink method, they are already compiled. ({{1+1}} is 2 in directive already).
This is how you would want to use:
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function ($parse) {
return function (scope, element, attr) {
element.val("value=" + $parse(attr.myDirective)(scope));
};
});
function MyCtrl($scope) {
$scope.aaa = 3432;
}​
.
<div ng-controller="MyCtrl">
<input my-directive="123">
<input my-directive="1+1">
<input my-directive="'1+1'">
<input my-directive="aaa">
</div>​​​​​​​​
One thing you should notice here is that, if you want set the value string, you should wrap it in quotes. (See 3rd input)
Here is the fiddle to play with: http://jsfiddle.net/neuTA/6/
Old Answer:
I'm not removing this for folks who can be misled like me, note that using $eval is perfectly fine the correct way to do it, but $parse has a different behavior, you probably won't need this to use in most of the cases.
The way to do it is, once again, using scope.$eval. Not only it compiles the angular expression, it has also access to the current scope's properties.
var myApp = angular.module('myApp',[]);
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
element.val("value = "+ scope.$eval(attr.value));
}
});
function MyCtrl($scope) {
}​
What you are missing was $eval.
http://docs.angularjs.org/api/ng.$rootScope.Scope#$eval
Executes the expression on the current scope returning the result. Any exceptions in the expression are propagated (uncaught). This is useful when evaluating angular expressions.

For an attribute value that needs to be interpolated in a directive that is not using an isolated scope, e.g.,
<input my-directive value="{{1+1}}">
use Attributes' method $observe:
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
attr.$observe('value', function(actual_value) {
element.val("value = "+ actual_value);
})
}
});
From the directive page,
observing interpolated attributes: Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.
If the attribute value is just a constant, e.g.,
<input my-directive value="123">
you can use $eval if the value is a number or boolean, and you want the correct type:
return function (scope, element, attr) {
var number = scope.$eval(attr.value);
console.log(number, number + 1);
});
If the attribute value is a string constant, or you want the value to be string type in your directive, you can access it directly:
return function (scope, element, attr) {
var str = attr.value;
console.log(str, str + " more");
});
In your case, however, since you want to support interpolated values and constants, use $observe.

The other answers here are very much correct, and valuable. But sometimes you just want simple: to get a plain old parsed value at directive instantiation, without needing updates, and without messing with isolate scope. For instance, it can be handy to provide a declarative payload into your directive as an array or hash-object in the form:
my-directive-name="['string1', 'string2']"
In that case, you can cut to the chase and just use a nice basic angular.$eval(attr.attrName).
element.val("value = "+angular.$eval(attr.value));
Working Fiddle.

For the same solution I was looking for Angularjs directive with ng-Model.
Here is the code that resolve the problem.
myApp.directive('zipcodeformatter', function () {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function (v) {
if (v) {
console.log('value changed, new value is: ' + v + ' ' + v.length);
if (v.length > 5) {
var newzip = v.replace("-", '');
var str = newzip.substring(0, 5) + '-' + newzip.substring(5, newzip.length);
element.val(str);
} else {
element.val(v);
}
}
});
}
};
});
HTML DOM
<input maxlength="10" zipcodeformatter onkeypress="return isNumberKey(event)" placeholder="Zipcode" type="text" ng-readonly="!checked" name="zipcode" id="postal_code" class="form-control input-sm" ng-model="patient.shippingZipcode" required ng-required="true">
My Result is:
92108-2223

var myApp = angular.module('myApp',[]);
myApp .directive('myDirective', function ($timeout) {
return function (scope, element, attr) {
$timeout(function(){
element.val("value = "+attr.value);
});
}
});
function MyCtrl($scope) {
}
Use $timeout because directive call after dom load so your changes doesn`'t apply

Related

What is the point of controller.$viewValue/controller.$modelValue?

I'm unclear what the relation is between scope.ngModel and controller.$viewValue/controller.$modelValue/controller.$setViewValue() is, and specifically, what the point of the latter three is. For example, see this jsfiddle:
<input type="text" ng-model="foo" my-directive>
and:
myApp.directive('myDirective', function($timeout) {
return {
require: 'ngModel',
restrict: 'A',
scope: { ngModel: '=' },
link: function (scope, element, attrs, controller) {
function log() {
console.log(scope.ngModel);
console.log(controller.$viewValue);
console.log(controller.$modelValue);
}
log();
controller.$setViewValue("boorb");
log();
scope.$watch('ngModel', function (val) {
console.log("val is now", val);
});
$timeout(function () {
log();
}, 2000);
}
}
});
With the controller being:
function MyCtrl($scope, $timeout) {
$scope.foo = 'ahha';
$timeout(function () {
$scope.foo = "good";
}, 1000);
}
The output is:
(index):45 ahha
(index):46 NaN
(index):47 NaN
(index):45 ahha
(index):46 boorb
(index):47 boorb
(index):53 val is now ahha
(index):53 val is now good
(index):45 good
(index):46 boorb
(index):47 boorb
controller.$viewValue did not start out as the value of the foo variable. Further, controller.$setViewValue("boorb") didn't influence scope.ngModel at all, nor was the update reflected in the HTML. Thus it seems there is no relation between scope.ngModel and controller.$viewValue. It seems that with anything I'd want to do, I would just use scope.ngModel, and watch those values. What is ever the point of using controller.$viewValue and controller.$modelValue or keeping them up to date with scope.ngModel?
scope: { ngModel: '=' }, creates an isolated scope for the directive, which means that changes to foo in the directive will no longer be reflected in the parent scope of MyCtrl.
Also, changes made by $setViewValue() will not get reflected in the DOM until controller.$render() is called, which tells Angular to update the DOM in the next digest cycle.
But to answer the question, NgModelController and its methods are really only necessary if you need to create some extra-special-custom-fancy data-binding directives. For normal data input and validation, you shouldn't ever need to use it. From the documentation (emphasis mine):
[NgModelController] contains services for data-binding, validation, CSS updates, and value formatting and parsing. It purposefully does not contain any logic which deals with DOM rendering or listening to DOM events. Such DOM related logic should be provided by other directives which make use of NgModelController for data-binding to control elements. Angular provides this DOM logic for most input elements.
The confusion here is coming from sticking a directive onto an existing directive, namely ngInput.
Instead, consider a fresh directive:
<my-directive ng-model="ugh">Sup</my-directive>
With:
$rootScope.ugh = 40;
And:
.directive('myDirective', function () {
return {
require: "ngModel",
// element-only directive
restrict: "E",
// template turns the directive into one input tag
// 'inner' is on the scope of the *directive*
template: "<input type='text' ng-model='inner'/>",
// the directive will have its own isolated scope
scope: { },
link: function (scope, element, attrs, ngModelCtrl) {
// formatter goes from modelValue (i.e. $rootScope.ugh) to
// view value (in this case, the string of twice the model
// value + '-'
ngModelCtrl.$formatters.push(function (modelValue) {
return ('' + (modelValue * 2)) + '-';
});
// render does what is necessary to display the view value
// in this case, sets the scope.inner so that the inner
// <input> can render it
ngModelCtrl.$render = function () {
scope.inner = ngModelCtrl.$viewValue;
};
// changes on the inner should trigger changes in the view value
scope.$watch('inner', function (newValue) {
ngModelCtrl.$setViewValue(newValue);
});
// when the view value changes, it gets parsed back into a model
// value via the parsers, which then sets the $modelValue, which
// then sets the underlying model ($rootScope.ugh)
ngModelCtrl.$parsers.push(function (viewValue) {
var sub = viewValue.substr(0, viewValue.length-1);
return parseInt(sub)/2;
});
}
};
})
Try it on Plunker.
Note that typeof ugh stays "number", even though the directive's view value is of a different type.

scope variable undefined in directive link function

I have a directive which has this piece of code:
return {
restrict: "E",
scope: {
moduleProgress: "=",
modules: "=",
parentScroll: "="
},
templateUrl: 'app/program/module.list.template.html',
link: function(scope,el,attrs,ctrl,tf) {
$templateRequest("app/program/chapter.list.template.html").then(function(template) {
var template = angular.element(template);
var elements = [];
console.log(scope);
attrs.$observe("modules", function(val) {
scope.modules.forEach(function(module) {
module.open = false;
});
});
For some reason, the first time I navigate to the view that contains this directive everything works fine, but from the second time onwards I always get a Cannot call method 'forEach' of undefined error. I read through some similar questions on SO which mentioned interpolated attributes not being instantly available on the link function, so they reccomended the use of $observe. I even added a console.log(scope) and the modules property shows up on the object. Any idea of what might be causing this?
It should be rather $scope.$watch rather that attrs.$observe as $observe required # in isolated scope of directive & on html it would be modules="{{module}}", For big object DOM will get messed up so I'd prefer you to use $watch which make sense
Code
scope.$watch("modules", function(newVal) {
if(newVal){
scope.modules.forEach(function(module) {
module.open = false;
});
}
});

Angular JS: How do I set a property on directive local scope that i can use in the template?

I want to access a variable in this case map in the directive without having to predefine it like setting it as an attr of the directrive map="someValue". And also i dont want to use scope.$apply because i actually only want the variable in the isolated scope of the directive. Is this even possible ?
What is the best practice here? Basically my directive needs to do both. Access the parent scope and have its own scope which with i can build the template with.
Thank you everybody.
Here my Js code:
.directive('myFilter', function() {
return {
restrict: 'E',
scope: {
source: '=source',
key: '=key',
},
link: function(scope, element, attrs) {
scope.$on('dataLoaded', function(e) {
scope.map = {};
angular.forEach(scope.source, function(paramObj) {
if (!scope.map[paramObj[scope.key]]) {
var newEntry = {
value: paramObj[scope.key],
isChecked: false
}
scope.map[paramObj[scope.key]] = newEntry;
}
});
});
}
}
});
and my html:
<my-filter source="metaPara.filteredParameters" key="'value'">
<table class="table table-borered">
<tr data-ng-repeat="item in map">
<td>{{item.value}}</td>
</tr>
</table>
</my-filter>
You might want to refer to the Angular documentation for directives, again.
If you want an isolate-scope (a scope which has no access to ancestors), then use
scope : { /* ... */ }
otherwise, if you want a unique scope, which does have access to ancestors, use
scope : true
Then, you can put your HTML-modifying or event-listening (that doesn't rely on ng-click or something else Angular already covers) in
link : function (scope, el, attrs, controller) { }
...and you can put all of your regular implementation inside of
controller : ["$scope", function ($scope) {
var myController = this;
myController.property = "12";
}],
controllerAs : "myController"
So that in your template you can say:
<span>{{ myController.property }}</span>
You can also use a pre-registered controller, which you call by name:
controller : "mySimpleController",
controllerAs : "myController"
Also, rather than using $scope.$apply, I'd recommend using $timeout (has to be injected).
The difference is that $scope.$apply will only work at certain points -- if you're already inside of a digest cycle, it will throw an error, and not update anything.
$timeout( ) sets the updates to happen during the next update-cycle.
Ideally, you should know whether or not you need an $apply or not, and be able to guarantee that you're only using it in one spot, per update/digest, but $timeout will save you from those points where you aren't necessarily sure.

Automatically pass $event with ng-click?

I know that I can get access to the click event from ng-click if I pass in the $event object like so:
<button ng-click="myFunction($event)">Give me the $event</button>
<script>
function myFunction (event) {
typeof event !== "undefined" // true
}
</script>
It's a little bit annoying having to pass $event explicitly every time. Is it possible to set ng-click to somehow pass it to the function by default?
Take a peek at the ng-click directive source:
...
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function(scope, element, attr) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
It shows how the event object is being passed on to the ng-click expression, using $event as a name of the parameter. This is done by the $parse service, which doesn't allow for the parameters to bleed into the target scope, which means the answer is no, you can't access the $event object any other way but through the callback parameter.
Add a $event to the ng-click, for example:
<button type="button" ng-click="saveOffer($event)" accesskey="S"></button>
Then the jQuery.Event was passed to the callback:
As others said, you can't actually strictly do what you are asking for. That said, all of the tools available to the angular framework are actually available to you as well! What that means is you can actually write your own elements and provide this feature yourself. I wrote one of these up as an example which you can see at the following plunkr (http://plnkr.co/edit/Qrz9zFjc7Ud6KQoNMEI1).
The key parts of this are that I define a "clickable" element (don't do this if you need older IE support). In code that looks like:
<clickable>
<h1>Hello World!</h1>
</clickable>
Then I defined a directive to take this clickable element and turn it into what I want (something that automatically sets up my click event):
app.directive('clickable', function() {
return {
transclude: true,
restrict: 'E',
template: '<div ng-transclude ng-click="handleClick($event)"></div>'
};
});
Finally in my controller I have the click event ready to go:
$scope.handleClick = function($event) {
var i = 0;
};
Now, its worth stating that this hard codes the name of the method that handles the click event. If you wanted to eliminate this, you should be able to provide the directive with the name of your click handler and "tada" - you have an element (or attribute) that you can use and never have to inject "$event" again.
Hope that helps!
I wouldn't recommend doing this, but you can override the ngClick directive to do what you are looking for. That's not saying, you should.
With the original implementation in mind:
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function(scope, element, attr) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
We can do this to override it:
// Go into your config block and inject $provide.
app.config(function ($provide) {
// Decorate the ngClick directive.
$provide.decorator('ngClickDirective', function ($delegate) {
// Grab the actual directive from the returned $delegate array.
var directive = $delegate[0];
// Stow away the original compile function of the ngClick directive.
var origCompile = directive.compile;
// Overwrite the original compile function.
directive.compile = function (el, attrs) {
// Apply the original compile function.
origCompile.apply(this, arguments);
// Return a new link function with our custom behaviour.
return function (scope, el, attrs) {
// Get the name of the passed in function.
var fn = attrs.ngClick;
el.on('click', function (event) {
scope.$apply(function () {
// If no property on scope matches the passed in fn, return.
if (!scope[fn]) {
return;
}
// Throw an error if we misused the new ngClick directive.
if (typeof scope[fn] !== 'function') {
throw new Error('Property ' + fn + ' is not a function on ' + scope);
}
// Call the passed in function with the event.
scope[fn].call(null, event);
});
});
};
};
return $delegate;
});
});
Then you'd pass in your functions like this:
<div ng-click="func"></div>
as opposed to:
<div ng-click="func()"></div>
jsBin: http://jsbin.com/piwafeke/3/edit
Like I said, I would not recommend doing this but it's a proof of concept showing you that, yes - you can in fact overwrite/extend/augment the builtin angular behaviour to fit your needs. Without having to dig all that deep into the original implementation.
Do please use it with care, if you were to decide on going down this path (it's a lot of fun though).

Callback function inside directive attr defined in different attr

So I have this directive called say, mySave, it's pretty much just this
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
element.bind("click", function() {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
}
});
}
});
the element itself looks like this
<button my-save my-save-callback="callbackFunctionInController()">save</button>
callbackFunctionInController is for now just
$scope.callbackFunctionInController = function() {
alert("callback");
}
when I console.log() attrs.mySaveCallback inside my-save directive, it just gives me a string callbackFunctionInController(), I read somewhere that I should $parse this and it would be fine, so I tried to $parse(attrs.mySaveCallback) which gave me back some function, but hardly the one I was looking for, it gave me back
function (a,b){return m(a,b)}
What am I doing wrong? Is this approach flawed from the beginning?
So what seems like the best way is using the isolated scope as suggested by ProLoser
app.directive('mySave', function($http) {
return {
scope: {
callback: '&mySaveCallback'
}
link: function(scope, element, attrs) {
element.on("click", function() {
$http.post('/save', scope.$parent.data).success(returnedData) {
// callback defined on my utils service here
scope.callback(); // fires alert
}
});
}
}
});
For passing parameters back to controller do this
[11:28] <revolunet> you have to send named parameters
[11:28] <revolunet> eg my-attr="callback(a, b)"
[11:29] <revolunet> in the directive: scope.callback({a:xxx, b:yyy})
There are a lot of ways to go about what you're doing. The FIRST thing you should know is that the $http.post() is going to be called as soon as that DOM element is rendered out by the template engine, and that's it. If you put it inside a repeat, the call will be done for each new item in the repeater, so my guess is this is definitely not what you want. And if it is then you really aren't designing things correctly because the existence of DOM alone should not dictate queries to the backend.
Anyway, directly answering your question; if you read the albeit crappy docs on $parse, it returns you an evaluation expression. When you execute this function by passing the scope to evaluate on, the current state of that expression on the scope you passed will be returned, this means your function will be executed.
var expression = $parse(attrs.mySave);
results = expression($scope); // call on demand when needed
expression.assign($scope, 'newValu'); // the major reason to leverage $parse, setting vals
Yes, it's a little confusing at first, but you must understand that a $scope changes constantly in asynchronous apps and it's all about WHEN you want the value determined, not just how. $parse is more useful for a reference to a model that you want to be able to assign a value to, not just read from.
Of course, you may want to read up on creating an isolate scope or on how to $eval() an expression.
$scope.$eval(attrs.mySave);
You can use .$eval to execute a statement in the given scope
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
scope.$eval(attrs.mySaveCallback)
}
}
});
TD: Demo
If you want to share data between a directive and a controller you can use the two way binding
app.controller('AppController', function ($scope) {
$scope.callbackFunctionInController = function() {
console.log('do something')
};
$scope.$watch('somedata', function(data) {
console.log('controller', data);
}, true);
});
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&mySaveCallback' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
scope.data = data;
scope.callback(); //calling callback, this may not be required
});
}
};
});
Demo: Fiddle
scope: {
callback: '&mySaveCallback'
}
Setting the scope explicitly could be a good solution but if you want the reach other parts of the original scope you can't because you have just overwritten it. For some reason, I needed to reach other parts of the scope too so I used the same implementation as ng-click do.
The use of my directive in HTML:
<div my-data-table my-source="dataSource" refresh="refresh(data)">
Inside the directive (without setting the scope explicitly):
var refreshHandler = $parse(attrs.refresh);
scope.$apply(function () {
refreshHandler( {data : conditions}, scope, { $event: event });
});
With this I can call the function in controller and pass parameters to it.
In the controller:
$scope.refresh= function(data){
console.log(data);
}
And it prints the conditions correctly out.
This worked for me
Inside the view script
<tag mycallbackattrib="scopemethod">
Inside the directive
$scope[attrs.mycallbackattrib](params....);
It is correctly called and params are passed, but maybe is not a best 'angular way' to work.
You should be using ng-click instead of creating your own directive.
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
if (scope.callback()) scope.callback().apply(data);
});
}
};
});

Categories

Resources