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.
Related
I have a directive that is currently working to change a HTML class element after you've scrolled passed a part of the page. I've essentially just hacked the code together from what I found on the internet and I am having trouble understanding why or how it is working. I know if I can understand it better I can attempt to recreate it for more meaningful aspects of my project. I would appreciate any insight someone could give. Below is the Angular part:
myApp.directive('changeClassOnScroll', function ($window) {
return {
restrict: 'A', // What does this line do?
scope: {
offset: "#", // A bit confused here
scrollClass: "#" // a bit confused here
},
link: function(scope, element) {
angular.element($window).bind("scroll", function() { // understood
if (this.pageYOffset >= parseInt(scope.offset)) { // understood
element.removeClass(scope.scrollClass); // understood
console.log('Scrolled below header.');
} else {
element.addClass(scope.scrollClass); // understood
}
});
}
};
})
In the HTML;
<nav change-class-on-scroll offset="100" scroll-class="navbar-transparent" class="navbar">
<!-- Don't understand why this works at all since these two elements are
<!-- not the same names as the function above? How does the directive
<!-- know to look for 'scroll-class' even tho in the directive it is
<!-- 'scrollClass' ?
Any help would really be much appreciated as to what is going on with it.
From the documentation
At a high level, directives are markers on a DOM element (such as an
attribute, element name, comment or CSS class) that tell AngularJS's
HTML compiler ($compile) to attach a specified behavior to that DOM
element (e.g. via event listeners), or even to transform the DOM
element and its children.
What you wrote is a standard angularjs code to create a custom directive that adds some functionalities to your dom.
restrict: 'A', // What does this line do?
'A' stands for attribute. Which means you can use this as an attribute of an html element like you used for your nav. You can use any of the following restrictions in a directive.
A - Attribute => <div change-class-on-scroll></div>
C - Class => <div class="change-class-on-scroll"></div>
E - Element => <change-class-on-scroll data="book_data"></change-class-on-scroll>
M - Comment => <!--directive:change-class-on-scroll --><br/>
scope: {
offset: "#", // A bit confused here
scrollClass: "#" // a bit confused here
},
'#' is used here to bind the data from your html to directives scope. With offset="100", you are making the value 100 to be available in the directives scope, and then when you call scope.offset in your link function, you'll get the value. You can use '#', '=' or '&' to bind values to the directive based on whether it is a definite value, model data or a function.
why scroll-class when in directive it is scrollClass
It works because that's how it should be. By Angularjs naming convention, the directive name and the scope objects to bind should be in camel case in your js and should be written using dashes in your html.
restrict: 'A', // this will restrict directive only as attribute(you can only use it as attribute or it defines directive type)
scope: {
offset: "#", // # means that the changes from the controller scope will be reflected in the directive scope but if you modify the value in the directive scope, the controller scope variable will not get affected.
scrollClass: "#"
},
There are Four types of directive elements (E), attributes (A), class names (C), and comments (M).A directive can specify which of the 4 matching types it supports in the restrict property of the directive definition object.
'#' is used to pass simple values in a directive.
In Html we cannot use camel case. Therefor we use snake case instead of camel case. Hence scrollClass will be written as scroll-class.
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.
I have a really weird Problem about formatters in nested directives.
I want a modelType directive for formatting.
If I don't check the "modelType", it works.
All View-Values are changed to "Formatted: ...".
But if i implement the if (attrs.modelType == "testType") { ... } it wont work, but i don't know why.
myApp.directive('modelType', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
if(attrs.modelType == "testType") {
ngModel.$formatters.push(function(value){
//formats the value for display when ng-model is changed
return 'Formatted: ' + value;
});
ngModel.$parsers.push(function(value){
//formats the value for ng-model when input value is changed
return value.slice(11);
});
}
}
};
Does anyone know this Problem?
http://jsfiddle.net/nkop2uq0/2/
First, using the attribute model-type both in the outer and inner directive led to the inner's link function getting executed twice (once for the controller-owned HTML, once for the template HTML), overwriting your formatting. So you need (or at least I think you should) disentagle the attribute for the two directives by using a different attribute for the inner directive:
template: '<div><input type="text" ng-model="ngModel" my-type="modelType" /></div>'
and consequently rename the inner directive to
myApp.directive('myType', function(){
Next, since the inner directive now does no longer get called by the compile step for the outer one, you need to compile the template for the inner directive in your post-link function
This can be done like this:
link: function (scope, element){
$compile(element.innerHtml)(scope)
}
This leads to attrs being always the same, so you can't use it to test for your testType:
$$element: U[1]
$$observers: Object
$attr: Object
myType: "modelType" <-- see here
ngModel: "ngModel"
type: "text"
Therefore you need to to look for the actual value inside scope:
if(scope.modelType == "testType") {
Lastly (and frankly I would be happy if someone could explain this to me, I don't understand it), I had to define the modelType scope property in the outer directive as one-way bound via
modelType: '#modelType'
I put this together in an updated fiddle here: http://jsfiddle.net/nkop2uq0/8/
NB: I am far from understanding the intricacies of Angular's directives. As much as I like the framework, the number of ways things can be achieved is mind-boggling. So you should try to improve my answer, it is most likely not best practice. And drop me a line if you find bad ideas in there...
I am struggling to get access to an isolated scope from some html I am building in the compile function of a directive. I have a SIMPLE example, the one I am working with is much more complex. Essentially I can't access properties on the isolated scope. Here is the code.
http://plnkr.co/edit/ETIRs4j3EwZ4DFN5XkPc?p=preview
It's odd to me because the in the console the scope looks great, from the post function.
Notice how the html is set up in the compile function:
tElement.append(angular.element('<div>{{name}}</div><span>{{value}}</span><span>Hello</span>'))
Also notice the second directive, Here with an inherited scope everything works fine.
Edit: I updated the first directive(The directive that isn't working) to set a property in the html so I could look for it in the console when I log the scope. It turns out it is on the $parent scope (The property is named "wat"). But why? It makes sense why it wouldn't work, but I don't understand why that html doesn't have access to the same isolated scope? Is there a way to force the html I am inserting to use the isolated scope instead?
Edit 2: Ok so a lot of questions about why I am trying to do this, the best description could be found here. https://github.com/angular/angular.js/issues/7874. Essentially we wanted to ng-repeat some transcluded content, and expose the item from ng-repeat to the transcluded content, but you can't do that in angular, because the transcluded content only has access to the parent scope.
The goal is to get the first directive to work like the second, in that the value of the two properties (value, and name) work from appending html in the compile function, with an isolated scope.
This is how I would go about what ( I think ) you are trying to achieve
http://plnkr.co/edit/mL7h7Hf8TkkpfsPNoL6g?p=preview
By using a template with transclude: true and a link function you can achieve the desired result as you replace the element with your template while interpreting the angular expressions
You could use replace: true to further customise your directive
app.directive('widget',function(){
return {
restrict:'E',
transclude: true,
template: '<span ng-bind="wat=\'YES\'"></span><div>{{name}}</div><span>{{value}}</span> <span>There</span>',
scope:{
name:'#',
value:'='
},
link:function($scope, tElement){
}
}
});
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.