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
Related
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.
(ANGULAR VERSION: 1.3.8)
I am getting an exception on a call within my directive's controller 'randomly'. The exception happens when the directive's controller gets called BEFORE the parent's controller and the parent controller scope variable used in the directive's controller is not yet initialized. However, on the 'random' occasion that my parent controller does get called BEFORE my directive's controller, and thus scope var is initialized, the call works and no exception is thrown. I have two questions (I have sample code below.):
Why is there 'randomness' in the sequence of calls between the directives's controller and the parent controller? I.e., why does one get called before/after the other?
I am able to eliminate the random sequence by a slight change in the HTML and by using ng-repeat on an array of length 1 in my html as opposed to passing in a single object to my directive. The directive code remains the same.
NOTE: The parent controller has a service to an ajax call injected (o_conversationService). The $scope.thread variable mentioned below relies on data returned from the ajax call. Perhaps, the randomness is rooted on the ajax callback timing although I'm not clear how. Regardless, this doesn't explain why the minor change in HTML removes the problem.
Below are two HTML snippets (the first working randomly and the second reliably) both using the same directive code:
HTML RANDOM (NOT)WORKING SCENARIO: With this HTML, the cfThread directive's controller gets called BEFORE parent controller and an exception is thrown because '$scope.thread' is null (see directive controller code below):
HTML snippet (directive name: 'cf-thread'):
<cf-thread thread = "o_conversationService.o_thread"></cf-thread>
HTML WORKING SCENARIO: With this HTML, the cfThreads directive controller gets called AFTER the parent's controller so '$scope.thread' is properly initialized (see directive controller code below):
HTML snippet (directive name: 'cf-thread'):
<cf-thread ng-repeat="threadObject in o_conversationService.o_threadCollection.objectDict" thread="threadObject"></cf-thread>
DIRECTIVE JS (Exception!!! marked below. Exception because $scope.thread is null):
threadDirectiveModule.directive('cfThread', ['$sce', function ($sce) {
return {
restrict: 'AEC',
scope: {
thread: '='
},
templateUrl: 'partials/directives/thread.html',
controller: function ($scope) {
//EXCEPTION!!! $scope.thread is null if parent controller
//wasn't called first.
var unsafeDirectLink = cf.Utilities.createExternalDirectLink($scope.thread.dataJSON.source.type,
$scope.thread.dataJSON.source.user_id,
$scope.thread.dataJSON.source.root_id)
if (cf.Utilities.isDefined(unsafeDirectLink)) {
$scope.thread.safeDirectLink = $sce.trustAsHtml(unsafeDirectLink);
}
else {
$scope.thread.safeDirectLink = null;
}
}
};
}]);
FINAL NOTE: In the non-working HTML scenario, I can always add a $watch to '$scope.thread' in the directive's controller. Needless to say, although it works, I would like to avoid it if not necessary, as seen the the working HTML scenario.
I appreciate your thoughts and patience!
I'm trying to come up with some code which allows me to run a function in the controller but only once the whole dom is setup and ready (including the directives link function run etc.).
I'm currently communicating between ctrl/service and the directive via $rootScope broadcasts. The first broadcast at the time of the controller loading is not being picked up by the directive. The reason is of course that the controller loads before the directive link function runs. I've read a few similar questions on SO where people recommended on using $timeout for these calls. This unfortunately doesn't always work and I don't want to clutter my ctrl/services with lots of $timeout calls. Therefore I'm looking for another solution to my problem.
Communication pattern is as follows:
1.) Controller tells Service to prepare some data (via function call in service)
2.) Service tells directive to display data (via broadcast)
3.) Directive displays data ...or not in my case :(
EDIT:
As timing is essential in my app, I'm basically looking for a way to initiate a function in the controller as soon as all angular components have finished loading. That function in the controller will display content by assigning a value to a scope variable. At the same time it will start taking the time. I can of course only start doing that once the directives are loaded, otherwise the tmining is wrong or the directive is not yet ready to display content etc.
I've read through a blog post by Ben Nadel, which basically shows how directives are loaded. I was hoping to setup an outer directive which loads last so I can trigger the finished loading from there. Unfortunately that doesn't work as soon as any of the inner directives use a templateUrl.
http://www.bennadel.com/blog/2603-directive-controller-and-link-timing-in-angularjs.htm
Using $timeout would be terrible. Don't do that. You can't define how long a server call is going to take.
I would recommend using this pattern:
Have the controller use a service to load some data, and have the
promise in the controller assign the return data to a scope variable.
Pass that scope variable into your directive.
Setup a watch in the directive link function, when it loads it will go from undefined to desired value. Done!
// in your controller
YourService.all().then(function(data) {
$scope.data = data;
});
// in your view
<some-directive your-data="data"></some-directive>
// in your directive
angular.module('blah.directives').directive('someDirective', function() {
return {
scope: {
yourData: '='
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('yourData', function() {
if(scope.yourData === undefined) return;
// at this point your data will be loaded, do work
// optionally kill watcher at this point if it's not going to update again
watcher();
});
}
}
});
Found some close questions , but not exactly the one I need to ask.
I have multiple elements with ng-click events.
For a majority of them (of a specific class), I don't need to actually run an angular digest cycle after click. The result of the click on these elements does not affect any scope variable (let's say for example they just print out a console.log).
What I want to do is to react conditionally to an ngClick, where say elements of a specific css class will not have the automatic $apply at the end.
Edit:
What I ended up doing was replace the ng-click, ng-mouseenter and ng-mouseleave with the corresponding javascript replacements.
I did this for two reasons:
1. I don't actually affect the scope variables on those clicks, so I don't need to run a digest after each (I have mouseenters, so you can imaging that generated a lot of digest cycles for no reason).
2. This is content that I load late in the page loading sequence from another source (ng-bind), so it has to be sanitized by angular and then compiled. This took a log of time (almost a second) because I have many such links, and that was holding back the display of the content.
While I highly recommend against this, as the $apply in your application shouldn't really be affecting anything (even performance). You'll have to create your own directive for this.
HTML
<div data-no-apply-click="myFunction()">
</div>
Javascript
.directive('noApplyClick', function ($parse) {
return {
compile : function ($element, attr) {
var fn = $parse(attr['noApplyClick']);
return function (scope, element, attr) {
element.on('click', function (event) {
fn(scope, {
$event : event,
$element : element
});
});
};
}
};
});
JsFiddle: http://jsfiddle.net/gj54bjsh/
In my angular app, directives are working fine during the first visit, but once a page been visited twice, all the directive link function gets called twice too. Say I am on page A, click a link to go to page B and then back to page A, all directives on page A will execute the their link function twice. if I refresh the browser it will become normal again.
Here is an example where the console.log will output twice when the second visit.
#app.directive 'testChart', ["SalesOrder", (SalesOrder) ->
return {
scope: {options: '='}
link: (scope, elem, attrs) ->
console.log("............checking")
SalesOrder.chart_data (data) ->
Morris.Line
element: "dash-sales"
data: data
xkey: 'purchased_at'
ykeys: ['total']
labels: ['Series a']
}
]
Any idea?
Update
My Route
when("/dash", {
templateUrl: "<%= asset_path('app/views/pages/dash.html') %>",
controller: DashCtrl
}).
so my chart is duplicated
also make sure you are not including your directive in your index.html TWICE!
I had this exact same problem.
After a loooooong time digging around I found that I hadn't used the correct closing tag which resulted in the chart being called twice.
I had
<line-chart><line-chart>
instead of
<line-chart></line-chart>
The link() function is called every time the element is to be bound to data in the $scope object.
Please check if you are fetching data multiple times , via GET call. You can monitor the resource fetching via Network tab , of chrome debugger.
A directive configures an HTML element and then updates that HTML subsequently whenever the $scope object changes.
A better name for the link() function would have been something like bind() or render(), which signals that this function is called whenever the directive needs to bind data to it, or to re-render it.
Maybe this will help somebody...
I had a problem with directive transclude, I used a transclude function which was adding child elements and also at the same time I forgot ng-transclude in directive template. Child elements were also directives and their link function was called twice!
Spent some time on this one..
More in details:
I had a "main" directive and "child" directives, idea was to use one inside another, something like that:
main
child
child
So problem was that link of "child" directive was called twice, and I didn't understand why,
Turned out I had ng-transclude in "main" directive template (I am posting it as it is in PUG format, sorry for that):
md-card(layout-fill)
md-card-content(flex)
.map-base(id="{{::config.id}}", layout-fill)
ng-transclude
and also in link function of "main" directive I called transclude function:
link: function($scope, $element, $attrs, controller, transcludeFn) {
$element.append(transcludeFn());
}
I think I just tried different combinations and forgot about that, visually everything was ok, but link was called twice and code was running twice and logic was broken..
So problem is that you can't have both and you have to choose one of the ways.
Hopefully now it is more clearer.
In my case I had a main-nav and sub-nav that both called a directive by its name attribute. Since the first instance already set the scope needed the second sub-nav the 2nd call wasn't needed. Incase anyone has a similar issue.