How to get evaluated attributes inside a custom directive
Asked Answered
L

5

365

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?

Lambert answered 11/9, 2012 at 13:44 Comment(1)
You can follow below link for better understanding about directives. undefinednull.com/2014/02/11/…Slagle
L
577

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.

Limousin answered 11/9, 2012 at 14:50 Comment(22)
Thanks for the reply, however this is not the solution. I've updated the fiddle with your code. jsfiddle.net/neuTA/3Lambert
In Chrome I get this error when trying to use scope.$parse: Object #<Object> has no method '$parse'. If I inject the $parse service -- function($parse) { return function (scope ... -- then try: "value = " + $parse(attr.value) -- that doesn't seem to work for me either.Eugenol
@Mark you are right, strange it works in the fiddle example (jsfiddle.net/neuTA/4) but not in the code I have ... angular versions?Lambert
@fastreload - do you have another idea?Lambert
I found out that a lot had changed since I started to work with "NG". so I guess this issue was resolved in the current version. I'll accept the answer and upgrade my angular version. thanks againLambert
@Shlomi, in the fiddle with scope.$parse(), jsfiddle.net/neuTA/4, I still get Chrome errors showing up in the console (hit F12 to see them). So, at least on my PC, although it appears to somewhat work (123 and 2 appear in the input fields), I don't think the linking function is executing -- which explains why I don't see, say, "value = 123" in the first input field. Rather, I only see "123".Eugenol
In the "Better answer" section, $scope.text will be undefined in the linking function. The way the answer is currently worded, it sounds like it would not be undefined. You have to use $observe() (or $watch() will actually work here too) to asynchronously see the interpolated value. See my answer and also #14876612Eugenol
Actually, it would be undefined in the initial state of the linking process, after that it would reflect the value it is supposed to reflect. It did not go into details of $watch and $observer for the sake of simplicity in the answer as its focus is on somewhere else; but yes, in order to follow value in the correct way, one is supposed to use either $watch or $observe depending on the circumstances.Process
Keep in mind that only one directive per element can request scope, so it may be best to avoid isolate scope except when absolutely necessary, in order to maximize the interoperability and reusability of your directives.Chappie
That depends on what do you want to do with that directive, it might be better to use isolate scope if you want to have a self-contained, pluggable directive.Process
In "Still OK" Answer it seems the $parse service is injected and then never used. Am I missing something?Postorbital
Can you please provide a working example on jsfiddle, codepen, or plunker? Thanks! (For you latest example.)Prosector
The "Still OK" solution is indeed wrong: the example doesn't event use $parse, and the jsfiddle is broken too. $parse(expression) returns a function, which you need to call with a context and another optional values. The usual use case is $parse(expression)(scope).Bahuvrihi
The Angular team should add this to the 'directive' documentation. Great job!Huygens
"Still OK" answer seems the only solution in case we need to evaluate variable parameters in case we can't use multiple isolated scope. As I know if we have multiple directive on the same element we can't have multiple isolated scope, so this appears to be the only solutionDegeneracy
For the "Still OK" why do you talk about scope.$eval and then never use it? Am I missing something implicit here?Degeneracy
@UmurKontacı, is there a suggestion for using directive type 'A' to pass value directly through it? Or there is no point and better way is to use separate parameter like you suggest in your Better answer?Snore
If you mean by restrict by directive type, the only thing that changes if from where you can define the directive. A stands for attribute so you can only init a directive like <div my-directive>, if it was E for element, you could init like <my-directive>.Process
I've seen varying answers to similar questions - this is due, in part, to differences between the current vs. previous angular versions - this video helped solidify my understanding: vimeo.com/95961018Martellato
The problem with "Better answer" is that in some cases You won't be able to create additional isolate scopes for that element if one already existsOvida
Is the @ / & / = thing documented anywhere?Lugo
Your "Better Answer" works only when your ATTRIBUTE restricted directive is used directly on a HTML element, once two different directives can not request an isolated scope on the same element, you wont be able to use this on a ELEMENT restricted directive. Which makes this pointless in any medium-to-major applications.Mislay
E
83

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.

Eugenol answered 12/9, 2012 at 19:49 Comment(3)
was this the only solution you found?Lambert
Yes, and since the directive page recommends this approach, this is how I would do it.Eugenol
+1, this is the best answer IMO since it doesn't force a scope on the directive and also covers attribute changes with $observeLocke
G
4

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.

Gauvin answered 5/4, 2014 at 16:0 Comment(3)
I don't know if you used an old angular version or what not, but all your code samples are either invalid javascript(my-directive-name=) or invalid angular (angular.$eval doesn't exist), so -1Locke
Ummm... given that this post is more than a year old, it wouldn't be at all surprising if something were since deprecated. However, a 10-second Google search would find you plenty of material on $eval, including right here at SO. And the other example you cite is an invocation in HTML, not Javascript.Gauvin
$scope.$eval(attr.val) works in angular 1.4. Requires $scope to be injected into the directive link function.Glassworker
F
4

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
Fiftyfifty answered 27/5, 2014 at 13:14 Comment(0)
M
2
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

Maracanda answered 2/9, 2015 at 7:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.