Javascript objects as function arguments in Angular directive properties - javascript

That title is a mouth-full! I've got some code written by another developer that's on leave and is unavailable to explain himself. None of us left in the office can figure out why it works. Any help would be appreciated!
We have a number of directives that use the results of a function defined in the controller as one of the properties. The method on the controller looks something like this.
$scope.required = function(pathString, object){
/*
pathString is a string representation pointing to a property in object
object is
Returns true/false for the property described
in the path string and the state of the object.
*/
}
Usually, when we use that as a property value for a directive implementation in an html file it looks like this.
<normal-directive
isrequired='required("Path.to.property", object)'></normal-directive>
The weird directive is unremarkable in their declaration. It's definition is identical to the normal directives.
directive('weirdDirective', function(){
return {
restrict: 'E',
scope: {
required: '&isrequired'
},
templateUrl: 'path/to/template'
}
});
The weirdness is in the implementation. Instead of the string and object passed to the function in the normal directives, these two new variable fieldpath and item appear.
<weird-directive
isrequired='required(fieldpath, item)'></weird-directive>
The variables fieldpath and item do not appear anywhere in the controller or the weird directive declaration. They are, however, present in the weird directive template, shown below.
<div class='weird-directive'>
<normal-directive
isrequired='required({fieldpath:'Path.to.property', item: object})'></normal-directive>
</div>
This raises ALL KINDS of questions for me. I thought the call to required was executed in the parent scope, so I don't understand how it's using variables that appear to be assigned a value in the template. Furthermore, objects are usually passed as a single argument, but they appear to have been split out here to provide the required two arguments.
I would very much like to understand how this works, so any help or explanation would be most appreciated!

"I thought the call to required was executed in the parent scope"
correct, the function is evaluated in the parent scope.
"so I don't understand how it's using variables that appear to be
assigned a value in the template"
Not exactly, what happening is slightly different.
Let's examine the DDO.
return {
restrict: 'E',
scope: {
required: '&isrequired'
},
templateUrl: 'path/to/template'
}
and the function in the parent scope:
$scope.required = function(pathString, object){
}
the & symbol means passing a reference to a function that, as you mentioned, is evalueted in the parent scope, however the documentation explain that if the method requires some parameters, you must pass along with the function the name of these parameters:
<weird-directive
isrequired='required(fieldpath, item)'></weird-directive>
I understand that the syntax could be a little misleading, but here we're not invoking the function, we are specifying that fieldpath and item are keys that will be used later during the invocation phase.
Now inside your child scope, you can invoke the function passing as an argument a map, where each key represent a parameter name, and the correspondent value the value itself.
in your case:
<normal-directive
isrequired='required({fieldpath:'Path.to.property', item: object})'></normal-directive>
last but not least, the evaluation of the function happens only after the evaluation of the parameters.
hope this helps.

Related

In declaring properties for a polymer element, why is value sometimes a function that returns {}?

The part I'm curious about is why use
value: function() { return {}; }
instead of
value: {}
Here's the sample code:
<script>
Polymer({
is: 'polymer-demo',
properties: {
data: {
type: Object,
notify: true,
value: function() { return {}; }
}
},
});
</script>
This is explained in the example:
When initializing a property to an object or array value, use a function to ensure that each element gets its own copy of the value, rather than having an object or array shared across all instances of the element.
Although you can use {}, this will be the same object shared by each element, so that if the value is mutated for one element, all other elements would see the same change applied to it. This is not what you want to happen. By using a function, the function will be called for each element, and each call will produce a new, separate object. Then a mutation will only apply to that single element without affecting the others.
I feel it is the same reason that vuejs requires their data component to be rendered as a function rather than the object that it is:
In the basic examples, we declare the data directly as a plain object.
This is because we are creating only a single instance with new Vue().
However, when defining a component, data must be declared as a
function that returns the initial data object. Why? Because there will
be many instances created using the same definition. If we still use a
plain object for data, that same object will be shared by reference
across all instance created! By providing a data function, every time
a new instance is created, we can simply call it to return a fresh
copy of the initial data.
Thus, making sure that the data being given to the component is always indicative of a fresh "load"
Hope this helps!
Usually this is done in cases where you want to show, that you can not only return a fixed value but calculate a dynamic value and return this instead but you were to lazy to return something more than {}. And sometimes the things you are declaring a used in some place by the framework and are there expected to be a function returning an object, and not just a fixed value or object.
You can find out by changing the code, executing it, and watching for errors or different behaviour.

What's the use of the "attrs" parameter in an angular directive link function

Considering this directive :
.directive('myDirective', function(httpRequestTracker) {
return {
restrict: 'EA',
scope: {
myvar: "=",
},
link: function($scope, elem, attrs) {
console.log($scope.myvar, attrs.myvar);
}
};
})
and this JS console output :
> undefined undefined // if no attribute
> value value // if attribute value = "value"
What is the use of the attr parameter of the link function ? What is the difference with $scope ?
Thanks
attrs is just a raw list of attributes on a directive
scope is more sophisticated, you can use three different operators to populate it with values:
= evaluates expression in HTML, and may contain objects
# interprets value passed as string. always.
& gives you access to functions defined on parent scope
I know I'm quite late to the party, but for posterity's sake, the most crucial difference to me is that using scope: {...} in the directive's definition object (not the link function signature) creates an isolate scope within the directive element, whereas omitting the scope property altogether and manually retrieving values from attrs does not. There are plenty of cases where, for whatever reason, you don't want to make a new scope but you still need the value of some attribute.

AngularJS: Using JS functions inside directives - bad practice?

I have this part of code inside ng-repeat:
<div ng-show="parameter == 'MyTESTtext'">{{parameter}}</div>
where parameter is some $scope string variable ...
I wanted to see if its possible to check (inside ng-show for instance) whether parameter contains a substring.
You could do this with:
<div ng-show="parameter.indexOf('TEST') != -1">{{parameter}}</div>
which seems to be working; it displays every parameter that contains 'TEST' keyword.
I was wondering:
is this a correct way of doing this within AngularJS app?
Is it OK to use javascript built in functions like that?
EDIT:
parameter is actually formed like this: (and is thus not a $scope variable as I said above, sorry)
<div ng-repeat="(parameter,value) in oneOfMyScopeArrays">
UPDATE
Since you're dealing with strings in ngRepeat and not objects, there's no place to set flag to in your data elements. In this case I would advise using a custom directive. I do not agree with Darryl Snow's opinion that directive in this case is redundant. With directive (as it was the case with flag in controller) you can evaluate parameter once instead of doing so in every $digest cycle. Furthermore, if you decide to implement same functionality in other template, instead of copying the expression around, which is redundant, you'd reuse same directive. Here's a quick idea of such directive:
.directive('parameter', function() {
return {
link: function($scope, $element, $attrs) {
$attrs.$observe('parameter', function(parameter) {
if (parameter.indexOf('TEST') == -1) {
$element.hide();
} else {
$element.text(parameter);
$element.show();
}
});
}
}
});
Template:
<div parameter="{{parameter}}"></div>
This directive even sets up one watcher less per parameter comparing to your original solution, which is better performance wise. On the other hand, it disables two-way binding (parameter text is rendered once), so it won't work in case you want to edit parameter string in place.
ORIGINAL ANSWER
Is it correct way? Technically yes, because it works. Is it OK? Not so much because of several reasons:
Performance. Everytime $digest loop runs (it might run quite a lot, depending on interactivity of application), it has to process every such expression. Therefore string parameter.indexOf('TEST') != -1 has to be parsed and evaluated, which means calling .indexOf up to several times after each interaction, for example click on element with ngClick directive. Wouldn't it be more performant to test this assumption parameter.indexOf('TEST') != -1 once in Controller and set a flag, e.g.
$scope.showParameter = parameter.indexOf('TEST') != -1
In template you would write
<div ng-show="showParameter">{{parameter}}</div>
Model logic in template. It's hard to tell the actual reasoning from your example when the parameter should be visible, but is it up to the template to have this logic? I think this belongs to controller, if not model to decide, that your view layer would be decoupled from making assumptions about how the model actually works.
Yes, perfectly ok I think. You could write a separate directive of your own that does the same thing - it may look a bit tidier but it's ultimately redundant when angular comes with ng-show already built in, and it means a slight additional payload to the user. You could also do a $scope.$watch on parameter and set another scope variable for ng-show, but that just moves the mess from your view to your controller.
$scope.$watch('parameter', function(){
if(parameter.indexOf('TEST') != -1)
$scope.showit = true;
});
and then in the view:
<div ng-show="showit">{{parameter}}</div>

How do I pass many values to an Angularjs directive?

I'm creating a reusable bit of html via a directive. The html would have a few variables that I want passed from the original scope. This is done easily by declaring attributes on the directive, then creating an isolate scope and capturing them. The question is, is there a better way to do this for a larger number of variables? I had thought of passing in an object like {firstAttr: $scope.one, secondAttr: $scope.two...} and picking this object apart to get each piece. This works the first time, but the two-way databinding doesn't work (even using the '=').
The problem is the thing that is bound is the object, not each of the individual parts of the object. Could I maybe use the compile function in the directive to add each of the attributes to the element or something? so:
<mydirective databinding="{one:'first one', two:'second one'}">
would be translated into:
<mydirective one="first one" two="second one">
That way my databinding would work as expected by capturing the attributes in the directive. How would I go about accomplishing that design, or is there just another way completely to do this?
The databinding directive idea is an interesting one but it's not the way I would do it since I believe you'd run into directive priority issues, plus the fact that it's very non-standard and would make your code hard to follow for future programmers. There's several ways to do this so I'll discuss 3 different solutions I've used.
Solution 1
If you only need one way data binding, the simplest solution is to use angular's scope.$eval function on the string representation of the object inside your directive after interpolating any simple scope variables on it using {{}}. The string representation doesn't even have to be valid JSON, since you'll notice in the example below I don't include quotes around the object keys.
In the view:
<div databinding="{one:'first', two:{{scopeVar}}, complex:[1,2, "Hi"]}"></div>
And in the javascript:
app.directive('databinding', function () {
return{
link: function (scope, elm, attrs) {
console.debug(scope.$eval(attrs['databinding']));
}
}
});
Solution 2
Another one-way data binding solution is to create an option object inside the controller and pass it to the directive using "#" (or even "="):
In the controller:
$scope.options = {one: "first, two: "second"};
In the view:
<div databinding="options"></div>
And in the javascript:
app.directive('databinding', function () {
return{
scope: {
options: "#" //Can also use = here
},
link: function (scope, elm, attrs) {
console.log(scope.options);
}
}
});
Solution 3
If you do need two way data binding, you're mostly out of luck, as there's no elegant way to do it. HOWEVER, if you're in the market for hackish solutions, you can accomplish two way data binding with a method very similar to solution 2 but with a change to the option object.
Instead of declaring an option object containing simple primitive data types like strings, create a dummy object inside the option object, which you then declare your variables inside of. Doing it this way, changes to scope variables in your controller will also be realized inside the directive, as demonstrated through the timeouts.
Controller:
$scope.someScopeVar = "Declared in controller"
$scope.options = {
dummy: {
one: $scope.someScopeVar,
two: "second"
}
}
window.setTimeout(function(){
$scope.someScopeVar = "Changed in controller";
}, 2000)
View:
<div databinding="options"></div>
Directive:
app.directive('databinding', function () {
return{
scope: {
options: "=" //You need to use = with this solution
},
link: function (scope, elm, attrs) {
console.log(scope.options.dummy.one); //Outputs "Declared in controller"
window.setTimeout(function(){
console.log(scope.options.dummy.one) //Outputs "Changed in controller"
}, 5000)
}
}
});
This method works since javascript passes objects by reference whereas primitives are copied. By nesting an object in an object the data binding is preserved.
You can change scope in directive as follows
.('mydirective ', function(){
var linker = function(scope, element){
console.log(scope.one, scope.two);
}
return {
link: linker,
scope: {one:"=", two:"="}
}
});
The question is, is there a better way to do this for a larger number of variables?
I don't think so. As you already found out, trying to pass them as one object results in the object being databound, not the individual parts.
Even if you could get something working with $compile, it would not be obvious to other people reading your code what is going on.
Another option is to either not create a scope (scope: false, which is the default for directives), or create a new child scope (scope: true), but require that the calling scope must use certain scope property names to use the directive. Then you don't have to specify any attributes. This makes the directive's use much more restrictive, but I think those are your two choice: specify multiple attributes, or require certain scope property names.

In AngularJS, how to make an isolated scope inherit from ng-repeat's scope

I'm trying to create a custom component that receives arguments in a ng-repeat loop.
So for example, say I have a component named "mycomp" that receives a custom argument "name" in a ng-repeat:
<mycomp name="{obj.name}" ng-repeat="obj in list" />
And in my directive the isolated scope is defined like this:
scope:{name:"#"}
That won't work because ng-repeat creates an isolated scope for each element it iterates. So I ended up having two levels of scopes.
How do I get around this issue? Am I doing something wrong?
Thanks.
As I stated in my comment of your original question, this has already been answered. Anyway, here it is, summed up:
In your template, state the model you want to have inherited, without {{}} (as using brackets results in the value being passed, and not the reference to the model itself):
<mycomp name="obj.name" ng-repeat="obj in list" />
And in your directive, establish a 2-way binding, like so:
scope:{name:"="}
EDIT:
I realize now (after your comment) that while this solves your problem, it doesn't fully answer the question. Here goes:
When you create a directive you have the choice of creating a scope that inherits from its parent (controller, typically, though not necessarily) ou an "isolated" scope, by specifying scope: true or scope: {...}, respectively.
So, by creating an unisolated scope, all the parent's models are available (you can access scope.obj - created via ng-repeat - but also scope.list). This is convenient, but also dangerous, of course (and doesn't really create reusable code).
If you create an isolated scope, you can specify the scope's models using '#', '=' or '&'.
'#' and '&' both produce a isolated, unbinded value, (that, if you change, changes only on the isolated scope - in your case, the object in the original list suffers no change at all), the only difference being that '#' reads a string value, and '&' reads an expression.
THIS IS IMPORTANT: the reason why I believe your code didn't work was (only) because you passed name="{obj.name}" and not name="{{obj.name}}", for with '#' the string value is read, and that string value can be the name of obj, but you must include it in {{}}!
If you use '=', you are declaring that you want that variable to be binded with the specified outside variable. So, if (in a fit of crazy, crazy rage!) you want to have 2 models in your directive that start up with the same value, but on is binded (i.e. changes are propagated to the outside scope), you could do something like this:
<mycomp binded-name="obj.name" unbinded-name="{{obj.name}}" ng-repeat="obj in list" />
and in your directive:
scope:{
bindedName: "=",
unbindedName: "#"
}

Categories