I just think the way I'm doing this sucks, and I'd like to know if there is a better way?
Here's the directive:
<myDirective myAttribute="{{val}}"></myDirective>
Here's the directive's controller:
.controller('myDirective', ['$scope', '$attrs', function ($scope, $attrs) {
$attrs.$observe('my-attribute', function (x) {
$scope.myAttribute = x; // yay we finally have the interpolated value...
});
This sucks for a number of reasons I don't want to get into. Is there a way to ensure that interpolated values are resolved before controller is called?
Ideally the $scope.myAttribute would have the interpolated value when the controller initializer is called.
EDIT: My primary goal is to get rid of the dependency on $attrs that this controller has.
Angular 1.2 RC2 or maybe RC3 broke a few things with attribute interpolation. See this bug I filed:
https://github.com/angular/angular.js/issues/4525
Which just got fixed today. You should never see double curly braces in your value, that's a bug.
However, as soon as an attribute is found to use interpolation, it's evaluation becomes asynchronous. Even with this bug fixed, you should see it's synchronous value as undefined (i.e. if you just read the value right out of $attrs). That's why you have to $observe, so the value gets handed to you as soon as it's available.
As to why it can't be available right away, it's dynamic. The evaluation of {{val}} might be different all the time. That's just the nature of Angular, everything live updates all the time.
Perhaps the best way is:
link: function (scope, element, attrs) {
attrs.$observe('myAttribute', function(x) {
scope.setMyAttribute(x);
});
}
And then:
.controller('myDirective', ['$scope', function ($scope) {
$scope.setMyAttribute = function (x) {
$scope.myAttribute = x; // yay we finally have the interpolated value...
});
EDIT: It took some doing... and here is a plunker demonstrating this bug:
http://plnkr.co/edit/p46zuYbFAFCYH394zrUY?p=preview
Note the importance of using "templateUrl" in the child directive. Using just "template" and the bug disappears.
Related
I am a rookie in angularJS learning about directives (and struggling a lot :)).
I am trying to understand a piece of angularJS code in the plunker
by user tasseKATT for the stack overflow question regarding angular-ui-bootstrap.
I was hoping if anyone can explain this code fragment in more detail.
Specifically
How parsing and compilation happens in directives
How angular knows when to recompile directives ($watch - perhaps, if so how).
I checked the documentation for $parse but dont see any explanation on the service taking a function. What piece of information am I missing.
Also what is the
(value || '').toString();
used for.
What are the properties compileHTML. Where can I see the documentation for the compile function explained in more detail than the one provided by AJS.
What is $$addBindingClass(tElement) and $$addBindingInfo.
Explain the function ngBindHtmlWatchAction
what is $sce.
The fragment from the directive is below
app.directive('compileHtml', ['$sce', '$parse', '$compile',
function($sce, $parse, $compile) {
return {
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.compileHtml);
var ngBindHtmlWatch = $parse(tAttrs.compileHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.compileHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
element.html($sce.trustAsHtml(ngBindHtmlGetter(scope)) || '');
$compile(element.contents())(scope);
});
};
}
};
}
]);
Disclaimer : First thing buddy, you are a rookie in angular and you are trying to understand directives using a very complicated directive. Maybe its better if you start with something a little less complicated :)
I will try to answer your many questions:
How parsing and compilation happens in directives:
Angular knows which all directives are present, so when you write a name which matches one of the directive it compiles the html and link the directive.(I am trying to find a good blog for you to read on these cycles of directive, maybe this post will help)
How angular knows when to recompile directives ($watch - perhaps, if so how): angular doesnot recompile directives, a directive is only compiled once when angular encounters it for the first time.
$watch is basically put on a variable, so whenever angularjs is applying the changes of the scope, it looks for that variable if it is changed.
$parse
It is a way of adding angular to html. So if you directly append html to an element, it does not have angular like features. So we get the html, compile/parse it and then append to the element. Parsing is basically getting all the variables in the html and applying two way binding on those, plus replacing them with their values in controller.
Read this post.
Remember that compile/parse are always done against a scope variable, because variables defined inside the html are properties of the scope variables.
What is this (value || '').toString();
This is a common coding practce of js developers.
This basically amounts to :
(value ? value : '').toString();
Why dont we use value directly like this
value.toString();
Because if the value is undefined or null, it does not have a toString function and it will throw an error.
So the coder is trying to check if the value is there, then convert the value to string otherwise, just put empty string(converting to string results empty string).
what is $sce?
sce is a service, that comes in file angular-sanitize.js which is a part of angular bundle. When somebody tries to modify html to insert his link(malicious activity we call it), angular does not allow and treats his html as simple text.
But what if you want to add html, you pass your html to $sce service(injected in controller/directive etc) and the output is the html which you can insert into the view(this will come as html).
$$addBindingClass
This is just to add "ng-binding" class to the element, so that you can see which element has a model created by angularjs.
$$addBindingInfo
This is to add information to the element for debugging purpose. You can select the element in the inspector and run the following statement in console to get the scope information.
angular.element($0).scope(); //$0 means selected element
Here is a link that explains this thing better.
I'm using Jasmine+Karma and need to find a way to test an angular directive used to alert the user if passwords don't match - it seems to accomplish this with a directive the renders true or false, and there is with ngShow on the HTML that displays when this, along with a couple other properties, are true.
Here's the directive. I'm having a little difficulty understanding how it works.
app.directive('passwordMatch', [function () {
return {
restrict: 'A',
scope:true,
require: 'ngModel',
link: function (scope, elem, attrs, control) {
var checker = function () {
var e1 = scope.$eval(attrs.ngModel);
var e2 = scope.$eval(attrs.passwordMatch);
if(e2!=null)
return e1 == e2;
};
scope.$watch(checker, function (n) {
control.$setValidity("passwordNoMatch", n);
});
}
};
}]);
<small class="errorMessage" data-ng-show="signupForm.password2.$dirty && signupForm.password2.$error.passwordNoMatch && !signupForm.password2.$error.required"> Password do not match.</small>
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM?
How does it do that, then when the purpose is to detect if passwords do not match - if they don't match, then e1 === e2 is false, and this value is passed into $scope.watch(checker, function(n)...? If that was how it worked, then wouldn't it set the of value passwordNoMatch to false, which would make ng-show hidden?
Or is that not how it works, it works another way?
And, before that, what's going on with the link: function part?
Where is the scope coming from (it just says scope:true in the directive?)
And the elem? And the attr (the attributes from html elements?)?
Is angular just looking through a list of each and every one of them, the elements and attributes, and the scope? Is there a passwordMatch property already in there somehow?
What is $eval doing?
You have a lot of questions here and spending some time with the Angular docs would help you answer a lot of them. I'll put links to the relevant docs so you can get a fuller explanation.
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM? How does it do that?
I think you are almost there. When you watch a function it gets called on every digest cycle and if the return value of the function has changed then it calls the function you passed as the second argument i.e.
function(n){
control.$setValidity("passwordNoMatch", n);
}
To understand this function you need to understand what the require option does. You have set require to 'ngModel' and basically what this means is that the control variable passed in as the 4th argument to your link function is a reference to NgModelController, which provides an API for the ngModel directive.
The $setValidity("passwordNoMatch", n) bit sets the value of the property 'passwordNoMatch' on the $error object to the value of n. So, n here is the return value of the function you are watching and the $error object is a property of the FormController that is available on all angular forms which have the name attribute defined in the HTML form tag. So, basically this function is what sets the value of the signupForm.password2.$error.passwordNoMatch that you see in your <small> tag.
Where is the scope coming from (it just says scope:true in the directive?)
The scope in link function is (from the Angular docs)..
The scope to be used by the directive for registering watches.
The scope: true bit is what tells Angular whether to create a new scope for the directive or to create an 'isolate scope' which does not 'prototypically inherit from the parent scope'. I would recommend that you spend some quality time reading about the directive definition object if you really want to grok directives.
What is $eval doing?
The first argument passed to scope.$eval is executed as an Angular Expression and the result is returned. So in your code I suspect they will both return strings from your password fields which you then check to see if they match.
Hope that helps.
I'm trying to figure out something with scope and link when a directive is initialized. I have a directive in a tree controller to display details at a branch point:
<typelists sub="branch.subBranches[0]"></typelists>
The (relevant parts of the) directive that handles that branch info are below:
listsApp.directive(
'typelists',
function($rootScope) {
return {
restrict: 'EA',
replace: true,
scope: {
branch : '=sub'
},
templateUrl: templateDir + '/typelists.html',
link: function (scope, element, attrs) {
console.log(scope,scope.branch); // DEBUG
// other stuff
// (including a working reload() and refresh() methods)
scope.subject = 'Type' + scope.branch.model.getFilter() + 'Lists';
// catch $rootScope.$broadcasts for this branch
$rootScope.$on( scope.subject+'Refresh', function() { scope.refresh(); ) } );
$rootScope.$on( scope.subject+'Reload', function() { scope.reload(); } );
}
};
Now, what is confusing the bajeezus out of me is that in the // DEBUG line, I can see .branch populated as expected in the output of the scope alone, but scope.branch shows as undefined.
This means that when I try to set scope.subject down below, instead of getting a typeId back from the parent type, I'm getting 'undefined' so instead of getting a unique branch tag such as 'Type1Lists' I'm getting 'TypeundefinedLists', thus my $on watch functions aren't triggering properly.
Why am I unable to access scope.branch or why is it showing as undefined when I try? (especially when I can see it in the same console.log output as part of scope?)
Thanks in advance for any help.
How does branch.subBranches[0] get populated? I bet that value is set just after the link function of the directive runs.
You can either make the directive resilient to these changes, like so:
var unwatch = scope.$watch("scope.branch", function(v){
if (v) {
unwatch(); // removes the watch when value is not undefined
init(); // run your init code
}
});
Or, only instantiate the directive when the data is ready:
<typelists ng-if="branch.subBranches[0] !== undefined"
sub="branch.subBranches[0]"></typelists>
P.S.
The reason console.log shows the data is because (at least in Chrome) the console "rendering" doesn't happen at the time of logging - in other words, when you call console.log the data is still not there, but it gets there before the console reads it for rendering purposes.
I would bet this is happening because you are setting branch.subBranches[0] in a parent directives link function.
However Angular links directives in a bottom-up manner. Namely the child directives link function will be called BEFORE the parents. Hence if you are setting 'branch.subBranches[0]' in the parent directives link function then it will still be undefined when the child directives link function is run (run first).
The timing of Angular directive DOM compilation is such that the controllers are run first from top-bottom (parent first), and then linked back up bottom-top (parent last).
So to fix your problem the easiest way would be to define/set branch.subBranches[0] in the parent's controller function (opposed to link).
EDIT:
Here is a plunker of what I suspect is happening in your case (open the console when running):
http://plnkr.co/edit/YMLAtGc38oc3vVqebH3Z?p=preview
Here is a plunker of the suggested fix:
http://plnkr.co/edit/EjrSorCvFLcDkODe2Anm?p=preview
I'm a 2 week old Angular noob, and I've been attempting my first ever directive for over a day now. :P I've read this and this, which seem good directives introductions and a bunch of Stackoverflow answers, but I can't get this working.
<div ng-app="App" ng-controller="Main">
<textarea caret caret-position="uiState.caretPosition"></textarea>
{{uiState.caretPosition}}
</div>
and
angular.module('App', [])
.controller('Main', ['$scope', function ($scope) {
$scope.uiState = {};
$scope.uiState.caretPosition = 0;
}])
.directive('caret', function() {
return {
restrict: 'A',
scope: {caretPosition: '='},
link: function(scope, element, attrs) {
var $el = angular.element(element);
$el.on('keyup', function() {
scope.caretPosition = $el.caret();
});
}
};
});
Fiddle is here. I'm basically trying to get the caret position within a textbox. I'm using this jQuery plugin in the fiddle (source of the .caret() method, which should just return a number).
My questions are
Do I even need a directive here? Ben Nadel says the best directives are the ones you don't have to write (amen to that!)
If yes, is this the right way to go about the directive? Ie isolated scope with two-way bound variable?
If yes, when I run this code locally, I keep getting the error Expression 'undefined' used with directive 'caret' is non-assignable!. I've read the doc and as far as I can tell I've followed instructions for how to fix to no avail.
BONUS: Why in jsfiddle do I get no such error. The fiddle just fails silently. What's with that?
Thanks!
My solution was harder to find out here, but easier to implement. I had to change it to the equivalent of;
scope: {caretPosition: '=?'},
(Note that the question mark makes the attribute optional. Prior to 1.5 this apparently wasn't required.)
You were close.. The main problem is changing a scope variable inside an event that angular doesn't know about. When that event occurs, you have to tell angular that something changed by using scope.$apply.
$el.on('keyup', function() {
scope.$apply( function() {
scope.caretPosition = $el.caret();
});
});
Working fiddle here
For the questions:
yes, I don't think there's a way around having to write a directive for this.
in this case, it seems fine.
not sure, there are no problems in the jsfiddle. It sounds like you're setting carat="something" somewhere? check to make sure the html is the same as what's in the fiddle.
same as 3
Also note that if you click and move the caret, it won't update because it's only listening for keyup.
I have to access variable defined in directive and access it in the controller using angularjs
directive :
app.directive('htmlData', function ($compile) {
return {
link: function($scope, element, attrs) {
$(element).on('click', function() {
$scope.html = $compile(element)($scope).html();
});
return $scope.html;
}
};
});
and use $scope.html in controller.
Since you are not creating an isolate scope (or a new scope) in your directive, the directive and the controller associated with the HTML where the directive is used are both using/sharing the same scope. $scope in the linking function and the $scope injected into the controller are the same. If you add a property to the scope in your linking function, the controller will be able to see it, and vice versa.
As you set the variable in the $scope, all you got to do is to bind to it normally. In your case, osmehting like:
<div html-data>{{html}}</div>
Maybe you're not seeing the update because it lacks a $scope.$apply().
Anyway, let me say that I see two problems on you code. First of, you could use ng-click directive to attach click events to your HTML.
Secondly, why would you recompile your HTML every time? There is almost no need for that. You may have a big problem, because after first compilation, your template is going to be lost, so recompiling will render it useless.
If you need to get the element, you can inject $element.