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();
});
}
}
});
Related
I need to require a property from a controller from another directive in my main directive, which is easy:
return {
...
require['myOtherController'],
link: function(scope,element,attrs,controller){
scope.propFromOtherCtrl = controller[0].propFromOtherCtrl;
}
But: the controller of my directive gets loaded first.
So at first, propFromOtherCtrl is undefined in the controller until the link function got executed.
Right now, i am $scope.$watching the property in my controller until it is defined and then kick off the initialization of my controller manually.
//directive controller
$scope.$watch(function(){return $scope.propFromOtherCtrl; },function(n,o){
if(n !== o){
// init
}
});
But that seems rather hacky. Is there a more elegant way of doing this?
I can not share the scope between the two directives directly because of the architecture of my app.
If the architecture is the only reason you cannot share the scope between two controllers, you can always use $controller to inherit one controller from the other, then you would have access to its scope regardless of the location in the html dom:
$controller('myOtherController', { $scope: $scope });
Another alternative is inserting the html part that triggers the directive into an ng-if that doesn't get initialized until the other controller is ready:
ng-if="propFromOtherCtrlLoaded"
Finally, in case neither of these suit you, using $watch is not that bad, except a small addition, stopping to listen to the changes, would make it more efficient:
var unreg = $scope.$watch(function(){return $scope.propFromOtherCtrl; },function(n,o){
if(n !== o){
// init
unreg();
}
});
I have a page that has multiple components and I've created each component as a directive. When the page is first loaded, that's when I grab all the data that should be available on the page. So all of the data exists on the controller for that route, which we'll just call pageCtrl. And then what I've been doing is binding any required data to each directive through the attributes, which of course ends up creating an isolate scope for each of them.
I know there are a few ways to share data, so given this situation, is there a recommended way of doing it? Or has anyone had better success doing it one particular way? While it's working perfectly fine the way I'm doing it, I've run into a few caveats. If I need just even one bit of information from the pageCtrl, I need to add another attribute to the directive. So it ends up creating more code on the directive element itself.
I was thinking about just creating a service that would store all the data, which the pageCtrl could initialize, instead of setting it on itself. Any feedback would be appreciated.
good question :)
First solution is to create in parent controller object and pass this object (via ng-model) to all directives. This object will be passed by reference (not by value) so controller and all directives will have access to the same object.
```
// in controller
$scope.shared_data = {someItems: []};
// in html
<my-directive ng-model=shared_data></my-directive>
Second solution is to create some simple service to store all of those data.
// in this solution you have to inject additional service to directive controller
(extended idea of point 2) creating service/factory that will be responsible by collecting and returning data. This service could be injected into directive and use the same methods to collect data. To avoid making multiple calls to API (REST) it could have some cache for each sensitive method.
Communication via events.... (probably the worsts solution for your example)
The first two ideas are probably the best, I do not know full specification of your product so final solution picking belongs to You:).
My advice is to try/play with all of those methods to really understand what is going on and how and when to use each of them :)
You can directly call your parent controller from child directive controller by using $parent.
App.controller('aCtrl', ['$scope', function ($scope) {
$scope.refresh=function(){
.......... //Updated Data get from DB
};
...........
}]);
App.directive('bDirective',function(){
restrict: 'EC',
replace: true,
scope: {},
controller: function($scope) {
$scope.$parent.refresh();
}
...
});
HTML:
<div ng-controller="aCtrl">
<div class="bDirective"></div> //directive
</div>
Method on $viewcontentloaded is firing asynchronously. To detail my problem, I have a variable in root scope i.e. my Main controller, which need to be initialized before my view controller loads. In module.run I am calling a sync function to initialize $rootScope.session. And In my view controller of a route, I am checking the status of session in afunction that is called like
$scope.$on('$viewContentLoaded', function() {
$scope.initialize();
});
But some times on page refreash, I am getting an undefined value for $rootScope.session, as It may have initialized later. So, Is there any way to make this synchronous like rootscope will be initialized before view loads. And for curiosity, how it will affect, if I call the $scope.initialize(); normally in my controller, in $viewContentLoaded or in $routeChangeSuccess.
Thanks in advance.
So, Is there any way to make this synchronous like rootscope will be initialized before view loads.
Use the $controller service to manually create the controller, as in a unit test.
$controllerProvider.register('FooCtrl', FooCtrl);
ctrl = $controller('FooCtrl', {$scope: scope});
Or $broadcast a custom event from the main controller down to the child:
function mainCtrl($rootScope)
{
$rootScope.$broadcast('abc');
}
function secondCtrl($scope)
{
$scope.$on('abc', function(event) { $scope.initialize(); });
}
Or use a try/catch block and a recursive call with a timer.
These are more or less the steps that you would take to implement lazy loading in AngularJS. In summary, you would first define your app module to keep instances of the relevant providers. Then you would define your lazy artifacts to register themselves using the providers rather than the module API. Then using a ‘resolve’ function that returns a promise in your route definition, you would load all lazy artifacts and resolve the promise once they have been loaded. This ensures that all lazy artifacts will be available before the relevant route is rendered. Also, don’t forget to resolve the promise inside $rootScope.$apply, if the resolution will be happening outside of AngularJS. Then you would create a ‘bootstrap’ script that first loads the app module before bootstrapping the app. Finally, you would link to the bootstrap script from your ‘index.html’ file.
References
AngularJS source: controllerSpec.js
Ifeanyi Isitor: Lazy Loading In AngularJS
AngularJS Lazy Loading with Require.js
Split Large AngularJS Controllers using the Mixin Pattern
I know that if I want to create a reusable item, such as a date picker, then creating it as a Directive is recommended.
However, let's say that on my homepage, I have a Welcome section that displays the quote of the day with a background image that comes from a Rest service. Should this be a Directive that can encapsulate the markup and controller logic? Or should it be a simple AngularJs Controller that binds to markup in my index.html?
What constitutes whether or not something should be created as a Directive?
Directive is only a wrapper for a controller. It means if you have a controller you can use it. But you also may use the same controller as a controller of a directive for example instead of link function use controller.
This allow as to draw clear line where to use directive and where to use a controller.
We have to use Controller if we want to reproduce logic of piece of HTML markup. When we want to use the same $scope assignments, the same functions inside $scope, ... but HTML markup is always different for every other place where we use this controller.
We have to use directive when we have same logic in a controller of a directive and same HTML markup.
So in your case it is definitely a directive.
This is my own common sense of course, and may not be ideal.
There are three things you will require to implement this functionality:
AngularJS Template a.k.a. Markup to display quote with an image next to it.
AngularJS service to encompass the REST call in order to fetch above details from the server.
AngularJS controller to consume the AngularJS service to feed the data back the template (point 1) to update it accordingly after every rest call.
So the fact is you can achieve this without even writing an AngularJS Directive but what if you need to replicate the same feature in many places. In that sense, you will probably have to copy the same template somewhere else which will again need a separate controller to consume the same service (as using the same controller multiple times in the DOM is not recommended and a bad practice).
With the Directive API, you can put the markup in a directive template and consume the service in a directive controller to render the UI. So the next time if you want multiple instance of the widget, you just have to inject the directive, that's it - rest will work without any issue.
App = angular.module('App', []);
App.directive('welcomeQuote', function(QuoteService) {
return {
restrict: 'E',
template: '<div><img ng-src="{{quote.img}}" /><span ng-bind="quote.title"></span></div>',
controller: function(scope) {
// returns {img: 'angular.png', title: 'AngularJS';
QuoteService.fetch().then(function(data) {
scope.quote = data;
});
}
}
});
App.factory('QuoteService', function($http) {
return function() {
fetch: function() {
return $http.get('http://quote-server.com/new')
}
};
});
Finally you can use the widget as:
<welecome-quote></welcome-quote>
I've been using directives in AngularJS which build a HTML element with data fetched from the $scope of the controller. I have my controller set a $scope.ready=true variable when it has fetched it's JSON data from the server. This way the directive won't have to build the page over and over each time data is fetched.
Here is the order of events that occur:
The controller page loads a route and fires the controller function.
The page scans the directives and this particular directive is fired.
The directive builds the element and evaluates its expressions and goes forward, but when the directive link function is fired, it waits for the controller to be "ready".
When ready, an inner function is fired which then continues building the partial.
This works, but the code is messy. My question is that is there an easier way to do this? Can I abstract my code so that it gets fired after my controller fires an event? Instead of having to make this onReady inner method.
Here's what it looks like (its works, but it's messy hard to test):
angular.module('App', []).directive('someDirective',function() {
return {
link : function($scope, element, attrs) {
var onReady = function() {
//now lets do the normal stuff
};
var readyKey = 'ready';
if($scope[readyKey] != true) {
$scope.$watch(readyKey, function() {
if($scope[readyKey] == true) {
onReady();
}
});
}
else {
onReady();
}
}
};
});
You could use $scope.$emit in your controller and $rootScope.on("bradcastEventName",...); in your directive. The good point is that directive is decoupled and you can pull it out from project any time. You can reuse same pattern for all directives and other "running" components of your app to respond to this event.
There are two issues that I have discovered:
Having any XHR requests fire in the background will not prevent the template from loading.
There is a difference between having the data be applied to the $scope variable and actually having that data be applied to the bindings of the page (when the $scope is digested). So if you set your data to the scope and then fire an event to inform the partial that the scope is ready then this won't ensure that the data binding for that partial is ready.
So to get around this, then the best solution is to:
Use this plugin to manage the event handling between the controller and any directives below:
https://github.com/yearofmoo/AngularJS-Scope.onReady
Do not put any data into your directive template HTML that you expect the JavaScript function to pickup and use. So if for example you have a link that looks like this:
<a data-user-id="{{ user_id }}" href="/path/to/:user_id/page">My Page</a>
Then the problem is that the directive will have to prepare the :user_id value from the data-user-id attribute, get the href value and replace the data. This means that the directive will have to continuously check the data-user-id attribute to see if it's there (by checking the attrs hash every few moments).
Instead, place a different scope variable directly into the URL
My Page
And then place this in your directive:
$scope.whenReady(function() {
$scope.directive_user_id = $scope.user_id;
});